Skip to content

Commit 0730906

Browse files
committed
Merge branch 'ps/mv-contradiction-fix'
"git mv a a/b dst" would ask to move the directory 'a' itself, as well as its contents, in a single destination directory, which is a contradicting request that is impossible to satisfy. This case is now detected and the command errors out. * ps/mv-contradiction-fix: builtin/mv: convert assert(3p) into `BUG()` builtin/mv: bail out when trying to move child and its parent
2 parents 4a4656d + 974f0d4 commit 0730906

File tree

2 files changed

+81
-7
lines changed

2 files changed

+81
-7
lines changed

builtin/mv.c

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ enum update_mode {
3939
INDEX = (1 << 2),
4040
SPARSE = (1 << 3),
4141
SKIP_WORKTREE_DIR = (1 << 4),
42+
/*
43+
* A file gets moved implicitly via a move of one of its parent
44+
* directories. This flag causes us to skip the check that we don't try
45+
* to move a file and any of its parent directories at the same point
46+
* in time.
47+
*/
48+
MOVE_VIA_PARENT_DIR = (1 << 5),
4249
};
4350

4451
#define DUP_BASENAME 1
@@ -183,6 +190,21 @@ static void remove_empty_src_dirs(const char **src_dir, size_t src_dir_nr)
183190
strbuf_release(&a_src_dir);
184191
}
185192

193+
struct pathmap_entry {
194+
struct hashmap_entry ent;
195+
const char *path;
196+
};
197+
198+
static int pathmap_cmp(const void *cmp_data UNUSED,
199+
const struct hashmap_entry *a,
200+
const struct hashmap_entry *b,
201+
const void *key UNUSED)
202+
{
203+
const struct pathmap_entry *e1 = container_of(a, struct pathmap_entry, ent);
204+
const struct pathmap_entry *e2 = container_of(b, struct pathmap_entry, ent);
205+
return fspathcmp(e1->path, e2->path);
206+
}
207+
186208
int cmd_mv(int argc,
187209
const char **argv,
188210
const char *prefix,
@@ -213,6 +235,8 @@ int cmd_mv(int argc,
213235
struct cache_entry *ce;
214236
struct string_list only_match_skip_worktree = STRING_LIST_INIT_DUP;
215237
struct string_list dirty_paths = STRING_LIST_INIT_DUP;
238+
struct hashmap moved_dirs = HASHMAP_INIT(pathmap_cmp, NULL);
239+
struct strbuf pathbuf = STRBUF_INIT;
216240
int ret;
217241

218242
git_config(git_default_config, NULL);
@@ -331,6 +355,7 @@ int cmd_mv(int argc,
331355

332356
dir_check:
333357
if (S_ISDIR(st.st_mode)) {
358+
struct pathmap_entry *entry;
334359
char *dst_with_slash;
335360
size_t dst_with_slash_len;
336361
int j, n;
@@ -348,6 +373,11 @@ int cmd_mv(int argc,
348373
goto act_on_entry;
349374
}
350375

376+
entry = xmalloc(sizeof(*entry));
377+
entry->path = src;
378+
hashmap_entry_init(&entry->ent, fspathhash(src));
379+
hashmap_add(&moved_dirs, &entry->ent);
380+
351381
/* last - first >= 1 */
352382
modes[i] |= WORKING_DIRECTORY;
353383

@@ -368,8 +398,7 @@ int cmd_mv(int argc,
368398
strvec_push(&sources, path);
369399
strvec_push(&destinations, prefixed_path);
370400

371-
memset(modes + argc + j, 0, sizeof(enum update_mode));
372-
modes[argc + j] |= ce_skip_worktree(ce) ? SPARSE : INDEX;
401+
modes[argc + j] = MOVE_VIA_PARENT_DIR | (ce_skip_worktree(ce) ? SPARSE : INDEX);
373402
submodule_gitfiles[argc + j] = NULL;
374403

375404
free(prefixed_path);
@@ -465,6 +494,32 @@ int cmd_mv(int argc,
465494
}
466495
}
467496

497+
for (i = 0; i < argc; i++) {
498+
const char *slash_pos;
499+
500+
if (modes[i] & MOVE_VIA_PARENT_DIR)
501+
continue;
502+
503+
strbuf_reset(&pathbuf);
504+
strbuf_addstr(&pathbuf, sources.v[i]);
505+
506+
slash_pos = strrchr(pathbuf.buf, '/');
507+
while (slash_pos > pathbuf.buf) {
508+
struct pathmap_entry needle;
509+
510+
strbuf_setlen(&pathbuf, slash_pos - pathbuf.buf);
511+
512+
needle.path = pathbuf.buf;
513+
hashmap_entry_init(&needle.ent, fspathhash(pathbuf.buf));
514+
515+
if (hashmap_get_entry(&moved_dirs, &needle, ent, NULL))
516+
die(_("cannot move both '%s' and its parent directory '%s'"),
517+
sources.v[i], pathbuf.buf);
518+
519+
slash_pos = strrchr(pathbuf.buf, '/');
520+
}
521+
}
522+
468523
if (only_match_skip_worktree.nr) {
469524
advise_on_updating_sparse_paths(&only_match_skip_worktree);
470525
if (!ignore_errors) {
@@ -507,7 +562,8 @@ int cmd_mv(int argc,
507562
continue;
508563

509564
pos = index_name_pos(the_repository->index, src, strlen(src));
510-
assert(pos >= 0);
565+
if (pos < 0)
566+
BUG("could not find source in index: '%s'", src);
511567
if (!(mode & SPARSE) && !lstat(src, &st))
512568
sparse_and_dirty = ie_modified(the_repository->index,
513569
the_repository->index->cache[pos],
@@ -589,6 +645,8 @@ int cmd_mv(int argc,
589645
strvec_clear(&dest_paths);
590646
strvec_clear(&destinations);
591647
strvec_clear(&submodule_gitfiles_to_free);
648+
hashmap_clear_and_free(&moved_dirs, struct pathmap_entry, ent);
649+
strbuf_release(&pathbuf);
592650
free(submodule_gitfiles);
593651
free(modes);
594652
return ret;

t/t7001-mv.sh

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -550,16 +550,32 @@ test_expect_success 'moving nested submodules' '
550550
git status
551551
'
552552

553-
test_expect_failure 'nonsense mv triggers assertion failure and partially updated index' '
553+
test_expect_success 'moving file and its parent directory at the same time fails' '
554554
test_when_finished git reset --hard HEAD &&
555555
git reset --hard HEAD &&
556556
mkdir -p a &&
557557
mkdir -p b &&
558558
>a/a.txt &&
559559
git add a/a.txt &&
560-
test_must_fail git mv a/a.txt a b &&
561-
git status --porcelain >actual &&
562-
grep "^A[ ]*a/a.txt$" actual
560+
cat >expect <<-EOF &&
561+
fatal: cannot move both ${SQ}a/a.txt${SQ} and its parent directory ${SQ}a${SQ}
562+
EOF
563+
test_must_fail git mv a/a.txt a b 2>err &&
564+
test_cmp expect err
565+
'
566+
567+
test_expect_success 'moving nested directory and its parent directory at the same time fails' '
568+
test_when_finished git reset --hard HEAD &&
569+
git reset --hard HEAD &&
570+
mkdir -p a/b/c &&
571+
>a/b/c/file.txt &&
572+
git add a &&
573+
mkdir target &&
574+
cat >expect <<-EOF &&
575+
fatal: cannot move both ${SQ}a/b/c${SQ} and its parent directory ${SQ}a${SQ}
576+
EOF
577+
test_must_fail git mv a/b/c a target 2>err &&
578+
test_cmp expect err
563579
'
564580

565581
test_done

0 commit comments

Comments
 (0)