Skip to content

Commit 0455aad

Browse files
committed
Merge branch 'sy/mv-out-of-cone'
"git mv A B" in a sparsely populated working tree can be asked to move a path between directories that are "in cone" (i.e. expected to be materialized in the working tree) and "out of cone" (i.e. expected to be hidden). The handling of such cases has been improved. * sy/mv-out-of-cone: mv: add check_dir_in_index() and solve general dir check issue mv: use flags mode for update_mode mv: check if <destination> exists in index to handle overwriting mv: check if out-of-cone file exists in index with SKIP_WORKTREE bit mv: decouple if/else-if checks using goto mv: update sparsity after moving from out-of-cone to in-cone t1092: mv directory from out-of-cone to in-cone t7002: add tests for moving out-of-cone file/directory
2 parents 73b9ef6 + b91a2b6 commit 0455aad

File tree

3 files changed

+284
-64
lines changed

3 files changed

+284
-64
lines changed

builtin/mv.c

Lines changed: 175 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,21 @@
1313
#include "string-list.h"
1414
#include "parse-options.h"
1515
#include "submodule.h"
16+
#include "entry.h"
1617

1718
static const char * const builtin_mv_usage[] = {
1819
N_("git mv [<options>] <source>... <destination>"),
1920
NULL
2021
};
2122

23+
enum update_mode {
24+
BOTH = 0,
25+
WORKING_DIRECTORY = (1 << 1),
26+
INDEX = (1 << 2),
27+
SPARSE = (1 << 3),
28+
SKIP_WORKTREE_DIR = (1 << 4),
29+
};
30+
2231
#define DUP_BASENAME 1
2332
#define KEEP_TRAILING_SLASH 2
2433

@@ -115,6 +124,36 @@ static int index_range_of_same_dir(const char *src, int length,
115124
return last - first;
116125
}
117126

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+
118157
int cmd_mv(int argc, const char **argv, const char *prefix)
119158
{
120159
int i, flags, gitmodules_modified = 0;
@@ -129,7 +168,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
129168
OPT_END(),
130169
};
131170
const char **source, **destination, **dest_path, **submodule_gitfile;
132-
enum update_mode { BOTH = 0, WORKING_DIRECTORY, INDEX, SPARSE } *modes;
171+
enum update_mode *modes;
133172
struct stat st;
134173
struct string_list src_for_dst = STRING_LIST_INIT_NODUP;
135174
struct lock_file lock_file = LOCK_INIT;
@@ -176,7 +215,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
176215
/* Checking */
177216
for (i = 0; i < argc; i++) {
178217
const char *src = source[i], *dst = destination[i];
179-
int length, src_is_dir;
218+
int length;
180219
const char *bad = NULL;
181220
int skip_sparse = 0;
182221

@@ -185,54 +224,103 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
185224

186225
length = strlen(src);
187226
if (lstat(src, &st) < 0) {
188-
/* only error if existence is expected. */
189-
if (modes[i] != SPARSE)
227+
int pos;
228+
const struct cache_entry *ce;
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+
}
243+
ce = active_cache[pos];
244+
if (!ce_skip_worktree(ce)) {
190245
bad = _("bad source");
191-
} else if (!strncmp(src, dst, length) &&
192-
(dst[length] == 0 || dst[length] == '/')) {
246+
goto act_on_entry;
247+
}
248+
if (!ignore_sparse) {
249+
string_list_append(&only_match_skip_worktree, src);
250+
goto act_on_entry;
251+
}
252+
/* Check if dst exists in index */
253+
if (cache_name_pos(dst, strlen(dst)) < 0) {
254+
modes[i] |= SPARSE;
255+
goto act_on_entry;
256+
}
257+
if (!force) {
258+
bad = _("destination exists");
259+
goto act_on_entry;
260+
}
261+
modes[i] |= SPARSE;
262+
goto act_on_entry;
263+
}
264+
if (!strncmp(src, dst, length) &&
265+
(dst[length] == 0 || dst[length] == '/')) {
193266
bad = _("can not move directory into itself");
194-
} else if ((src_is_dir = S_ISDIR(st.st_mode))
195-
&& lstat(dst, &st) == 0)
267+
goto act_on_entry;
268+
}
269+
if (S_ISDIR(st.st_mode)
270+
&& lstat(dst, &st) == 0) {
196271
bad = _("cannot move directory over file");
197-
else if (src_is_dir) {
272+
goto act_on_entry;
273+
}
274+
275+
dir_check:
276+
if (S_ISDIR(st.st_mode)) {
277+
int j, dst_len, n;
198278
int first = cache_name_pos(src, length), last;
199279

200-
if (first >= 0)
280+
if (first >= 0) {
201281
prepare_move_submodule(src, first,
202282
submodule_gitfile + i);
203-
else if (index_range_of_same_dir(src, length,
204-
&first, &last) < 1)
283+
goto act_on_entry;
284+
} else if (index_range_of_same_dir(src, length,
285+
&first, &last) < 1) {
205286
bad = _("source directory is empty");
206-
else { /* last - first >= 1 */
207-
int j, dst_len, n;
208-
209-
modes[i] = WORKING_DIRECTORY;
210-
n = argc + last - first;
211-
REALLOC_ARRAY(source, n);
212-
REALLOC_ARRAY(destination, n);
213-
REALLOC_ARRAY(modes, n);
214-
REALLOC_ARRAY(submodule_gitfile, n);
215-
216-
dst = add_slash(dst);
217-
dst_len = strlen(dst);
218-
219-
for (j = 0; j < last - first; j++) {
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);
225-
modes[argc + j] = ce_skip_worktree(ce) ? SPARSE : INDEX;
226-
submodule_gitfile[argc + j] = NULL;
227-
}
228-
argc += last - first;
287+
goto act_on_entry;
288+
}
289+
290+
/* last - first >= 1 */
291+
modes[i] |= WORKING_DIRECTORY;
292+
n = argc + last - first;
293+
REALLOC_ARRAY(source, n);
294+
REALLOC_ARRAY(destination, n);
295+
REALLOC_ARRAY(modes, n);
296+
REALLOC_ARRAY(submodule_gitfile, n);
297+
298+
dst = add_slash(dst);
299+
dst_len = strlen(dst);
300+
301+
for (j = 0; j < last - first; j++) {
302+
const struct cache_entry *ce = active_cache[first + j];
303+
const char *path = ce->name;
304+
source[argc + j] = path;
305+
destination[argc + j] =
306+
prefix_path(dst, dst_len, path + length + 1);
307+
memset(modes + argc + j, 0, sizeof(enum update_mode));
308+
modes[argc + j] |= ce_skip_worktree(ce) ? SPARSE : INDEX;
309+
submodule_gitfile[argc + j] = NULL;
229310
}
230-
} else if (!(ce = cache_file_exists(src, length, 0))) {
311+
argc += last - first;
312+
goto act_on_entry;
313+
}
314+
if (!(ce = cache_file_exists(src, length, 0))) {
231315
bad = _("not under version control");
232-
} else if (ce_stage(ce)) {
316+
goto act_on_entry;
317+
}
318+
if (ce_stage(ce)) {
233319
bad = _("conflicted");
234-
} else if (lstat(dst, &st) == 0 &&
235-
(!ignore_case || strcasecmp(src, dst))) {
320+
goto act_on_entry;
321+
}
322+
if (lstat(dst, &st) == 0 &&
323+
(!ignore_case || strcasecmp(src, dst))) {
236324
bad = _("destination exists");
237325
if (force) {
238326
/*
@@ -246,34 +334,40 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
246334
} else
247335
bad = _("Cannot overwrite");
248336
}
249-
} else if (string_list_has_string(&src_for_dst, dst))
337+
goto act_on_entry;
338+
}
339+
if (string_list_has_string(&src_for_dst, dst)) {
250340
bad = _("multiple sources for the same target");
251-
else if (is_dir_sep(dst[strlen(dst) - 1]))
341+
goto act_on_entry;
342+
}
343+
if (is_dir_sep(dst[strlen(dst) - 1])) {
252344
bad = _("destination directory does not exist");
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;
345+
goto act_on_entry;
346+
}
273347

274-
string_list_insert(&src_for_dst, dst);
348+
/*
349+
* We check if the paths are in the sparse-checkout
350+
* definition as a very final check, since that
351+
* allows us to point the user to the --sparse
352+
* option as a way to have a successful run.
353+
*/
354+
if (!ignore_sparse &&
355+
!path_in_sparse_checkout(src, &the_index)) {
356+
string_list_append(&only_match_skip_worktree, src);
357+
skip_sparse = 1;
275358
}
359+
if (!ignore_sparse &&
360+
!path_in_sparse_checkout(dst, &the_index)) {
361+
string_list_append(&only_match_skip_worktree, dst);
362+
skip_sparse = 1;
363+
}
364+
365+
if (skip_sparse)
366+
goto remove_entry;
367+
368+
string_list_insert(&src_for_dst, dst);
276369

370+
act_on_entry:
277371
if (!bad)
278372
continue;
279373
if (!ignore_errors)
@@ -304,11 +398,17 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
304398
const char *src = source[i], *dst = destination[i];
305399
enum update_mode mode = modes[i];
306400
int pos;
401+
struct checkout state = CHECKOUT_INIT;
402+
state.istate = &the_index;
403+
404+
if (force)
405+
state.force = 1;
307406
if (show_only || verbose)
308407
printf(_("Renaming %s to %s\n"), src, dst);
309408
if (show_only)
310409
continue;
311-
if (mode != INDEX && mode != SPARSE && rename(src, dst) < 0) {
410+
if (!(mode & (INDEX | SPARSE | SKIP_WORKTREE_DIR)) &&
411+
rename(src, dst) < 0) {
312412
if (ignore_errors)
313413
continue;
314414
die_errno(_("renaming '%s' failed"), src);
@@ -322,12 +422,23 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
322422
1);
323423
}
324424

325-
if (mode == WORKING_DIRECTORY)
425+
if (mode & (WORKING_DIRECTORY | SKIP_WORKTREE_DIR))
326426
continue;
327427

328428
pos = cache_name_pos(src, strlen(src));
329429
assert(pos >= 0);
330430
rename_cache_entry_at(pos, dst);
431+
432+
if ((mode & SPARSE) &&
433+
(path_in_sparse_checkout(dst, &the_index))) {
434+
int dst_pos;
435+
436+
dst_pos = cache_name_pos(dst, strlen(dst));
437+
active_cache[dst_pos]->ce_flags &= ~CE_SKIP_WORKTREE;
438+
439+
if (checkout_entry(active_cache[dst_pos], &state, NULL, NULL))
440+
die(_("cannot checkout %s"), active_cache[dst_pos]->name);
441+
}
331442
}
332443

333444
if (gitmodules_modified)

t/t1092-sparse-checkout-compatibility.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1828,4 +1828,29 @@ test_expect_success 'checkout behaves oddly with df-conflict-2' '
18281828
test_cmp full-checkout-err sparse-index-err
18291829
'
18301830

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
1835+
# sparse index).
1836+
test_all_match git mv --sparse folder1 deep &&
1837+
test_all_match git status --porcelain=v2 &&
1838+
test_sparse_match git ls-files -t &&
1839+
git -C sparse-checkout ls-files -t >actual &&
1840+
grep -e "H deep/folder1/0/0/0" actual &&
1841+
grep -e "H deep/folder1/0/1" actual &&
1842+
grep -e "H deep/folder1/a" actual &&
1843+
1844+
test_all_match git reset --hard &&
1845+
1846+
# <source> as a directory deeper than sparse index boundary (where
1847+
# sparse index will expand).
1848+
test_sparse_match git mv --sparse folder1/0 deep &&
1849+
test_sparse_match git status --porcelain=v2 &&
1850+
test_sparse_match git ls-files -t &&
1851+
git -C sparse-checkout ls-files -t >actual &&
1852+
grep -e "H deep/0/0/0" actual &&
1853+
grep -e "H deep/0/1" actual
1854+
'
1855+
18311856
test_done

0 commit comments

Comments
 (0)