INDEX = (1 << 2),
SPARSE = (1 << 3),
SKIP_WORKTREE_DIR = (1 << 4),
+ /*
+ * A file gets moved implicitly via a move of one of its parent
+ * directories. This flag causes us to skip the check that we don't try
+ * to move a file and any of its parent directories at the same point
+ * in time.
+ */
+ MOVE_VIA_PARENT_DIR = (1 << 5),
};
#define DUP_BASENAME 1
strbuf_release(&a_src_dir);
}
+struct pathmap_entry {
+ struct hashmap_entry ent;
+ const char *path;
+};
+
+static int pathmap_cmp(const void *cmp_data UNUSED,
+ const struct hashmap_entry *a,
+ const struct hashmap_entry *b,
+ const void *key UNUSED)
+{
+ const struct pathmap_entry *e1 = container_of(a, struct pathmap_entry, ent);
+ const struct pathmap_entry *e2 = container_of(b, struct pathmap_entry, ent);
+ return fspathcmp(e1->path, e2->path);
+}
+
int cmd_mv(int argc,
const char **argv,
const char *prefix,
struct cache_entry *ce;
struct string_list only_match_skip_worktree = STRING_LIST_INIT_DUP;
struct string_list dirty_paths = STRING_LIST_INIT_DUP;
+ struct hashmap moved_dirs = HASHMAP_INIT(pathmap_cmp, NULL);
+ struct strbuf pathbuf = STRBUF_INIT;
int ret;
git_config(git_default_config, NULL);
dir_check:
if (S_ISDIR(st.st_mode)) {
+ struct pathmap_entry *entry;
char *dst_with_slash;
size_t dst_with_slash_len;
int j, n;
goto act_on_entry;
}
+ entry = xmalloc(sizeof(*entry));
+ entry->path = src;
+ hashmap_entry_init(&entry->ent, fspathhash(src));
+ hashmap_add(&moved_dirs, &entry->ent);
+
/* last - first >= 1 */
modes[i] |= WORKING_DIRECTORY;
strvec_push(&sources, path);
strvec_push(&destinations, prefixed_path);
- memset(modes + argc + j, 0, sizeof(enum update_mode));
- modes[argc + j] |= ce_skip_worktree(ce) ? SPARSE : INDEX;
+ modes[argc + j] = MOVE_VIA_PARENT_DIR | (ce_skip_worktree(ce) ? SPARSE : INDEX);
submodule_gitfiles[argc + j] = NULL;
free(prefixed_path);
}
}
+ for (i = 0; i < argc; i++) {
+ const char *slash_pos;
+
+ if (modes[i] & MOVE_VIA_PARENT_DIR)
+ continue;
+
+ strbuf_reset(&pathbuf);
+ strbuf_addstr(&pathbuf, sources.v[i]);
+
+ slash_pos = strrchr(pathbuf.buf, '/');
+ while (slash_pos > pathbuf.buf) {
+ struct pathmap_entry needle;
+
+ strbuf_setlen(&pathbuf, slash_pos - pathbuf.buf);
+
+ needle.path = pathbuf.buf;
+ hashmap_entry_init(&needle.ent, fspathhash(pathbuf.buf));
+
+ if (hashmap_get_entry(&moved_dirs, &needle, ent, NULL))
+ die(_("cannot move both '%s' and its parent directory '%s'"),
+ sources.v[i], pathbuf.buf);
+
+ slash_pos = strrchr(pathbuf.buf, '/');
+ }
+ }
+
if (only_match_skip_worktree.nr) {
advise_on_updating_sparse_paths(&only_match_skip_worktree);
if (!ignore_errors) {
strvec_clear(&dest_paths);
strvec_clear(&destinations);
strvec_clear(&submodule_gitfiles_to_free);
+ hashmap_clear_and_free(&moved_dirs, struct pathmap_entry, ent);
+ strbuf_release(&pathbuf);
free(submodule_gitfiles);
free(modes);
return ret;
git status
'
-test_expect_failure 'nonsense mv triggers assertion failure and partially updated index' '
+test_expect_success 'moving file and its parent directory at the same time fails' '
test_when_finished git reset --hard HEAD &&
git reset --hard HEAD &&
mkdir -p a &&
mkdir -p b &&
>a/a.txt &&
git add a/a.txt &&
- test_must_fail git mv a/a.txt a b &&
- git status --porcelain >actual &&
- grep "^A[ ]*a/a.txt$" actual
+ cat >expect <<-EOF &&
+ fatal: cannot move both ${SQ}a/a.txt${SQ} and its parent directory ${SQ}a${SQ}
+ EOF
+ test_must_fail git mv a/a.txt a b 2>err &&
+ test_cmp expect err
+'
+
+test_expect_success 'moving nested directory and its parent directory at the same time fails' '
+ test_when_finished git reset --hard HEAD &&
+ git reset --hard HEAD &&
+ mkdir -p a/b/c &&
+ >a/b/c/file.txt &&
+ git add a &&
+ mkdir target &&
+ cat >expect <<-EOF &&
+ fatal: cannot move both ${SQ}a/b/c${SQ} and its parent directory ${SQ}a${SQ}
+ EOF
+ test_must_fail git mv a/b/c a target 2>err &&
+ test_cmp expect err
'
test_done