diff --git a/builtin-mv.c b/builtin-mv.c index 593ff9e434..42c2e39b21 100644 --- a/builtin-mv.c +++ b/builtin-mv.c @@ -42,6 +42,18 @@ static void show_list(const char *label, struct path_list *list) } } +static const char *add_slash(const char *path) +{ + int len = strlen(path); + if (path[len - 1] != '/') { + char *with_slash = xmalloc(len + 2); + memcpy(with_slash, path, len); + strcat(with_slash + len, "/"); + return with_slash; + } + return path; +} + static struct lock_file lock_file; int cmd_mv(int argc, const char **argv, char **envp) @@ -50,6 +62,7 @@ int cmd_mv(int argc, const char **argv, char **envp) int verbose = 0, show_only = 0, force = 0, ignore_errors = 0; const char *prefix = setup_git_directory(); const char **source, **destination, **dest_path; + enum update_mode { BOTH = 0, WORKING_DIRECTORY, INDEX } *modes; struct stat st; struct path_list overwritten = {NULL, 0, 0, 0}; struct path_list src_for_dst = {NULL, 0, 0, 0}; @@ -94,11 +107,14 @@ int cmd_mv(int argc, const char **argv, char **envp) usage(builtin_mv_usage); source = copy_pathspec(prefix, argv + i, count, 0); + modes = xcalloc(count, sizeof(enum update_mode)); dest_path = copy_pathspec(prefix, argv + argc - 1, 1, 0); - if (!lstat(dest_path[0], &st) && S_ISDIR(st.st_mode)) + if (!lstat(dest_path[0], &st) && + S_ISDIR(st.st_mode)) { + dest_path[0] = add_slash(dest_path[0]); destination = copy_pathspec(dest_path[0], argv + i, count, 1); - else { + } else { if (count != 1) usage(builtin_mv_usage); destination = dest_path; @@ -114,7 +130,64 @@ int cmd_mv(int argc, const char **argv, char **envp) if (lstat(source[i], &st) < 0) bad = "bad source"; - else if (lstat(destination[i], &st) == 0) { + + if (S_ISDIR(st.st_mode)) { + const char *dir = source[i], *dest_dir = destination[i]; + int first, last, len = strlen(dir); + + if (lstat(dest_dir, &st) == 0) { + bad = "cannot move directory over file"; + goto next; + } + + modes[i] = WORKING_DIRECTORY; + + first = cache_name_pos(source[i], len); + if (first >= 0) + die ("Huh? %s/ is in index?", dir); + + first = -1 - first; + for (last = first; last < active_nr; last++) { + const char *path = active_cache[last]->name; + if (strncmp(path, dir, len) || path[len] != '/') + break; + } + + if (last - first < 1) + bad = "source directory is empty"; + else if (!bad) { + int j, dst_len = strlen(dest_dir); + + if (last - first > 0) { + source = realloc(source, + (count + last - first) + * sizeof(char *)); + destination = realloc(destination, + (count + last - first) + * sizeof(char *)); + modes = realloc(modes, + (count + last - first) + * sizeof(enum update_mode)); + } + + dest_dir = add_slash(dest_dir); + + for (j = 0; j < last - first; j++) { + const char *path = + active_cache[first + j]->name; + source[count + j] = path; + destination[count + j] = + prefix_path(dest_dir, dst_len, + path + len); + modes[count + j] = INDEX; + } + count += last - first; + } + + goto next; + } + + if (!bad && lstat(destination[i], &st) == 0) { bad = "destination exists"; if (force) { /* @@ -147,6 +220,7 @@ int cmd_mv(int argc, const char **argv, char **envp) path_list_insert(destination[i], &src_for_dst); } +next: if (bad) { if (ignore_errors) { if (--count > 0) { @@ -157,7 +231,7 @@ int cmd_mv(int argc, const char **argv, char **envp) (count - i) * sizeof(char *)); } } else - die ("Error: %s, source=%s, destination=%s", + die ("%s, source=%s, destination=%s", bad, source[i], destination[i]); } } @@ -166,12 +240,15 @@ int cmd_mv(int argc, const char **argv, char **envp) if (show_only || verbose) printf("Renaming %s to %s\n", source[i], destination[i]); - if (!show_only && + if (!show_only && modes[i] != INDEX && rename(source[i], destination[i]) < 0 && !ignore_errors) die ("renaming %s failed: %s", source[i], strerror(errno)); + if (modes[i] == WORKING_DIRECTORY) + continue; + if (cache_name_pos(source[i], strlen(source[i])) >= 0) { path_list_insert(source[i], &deleted); diff --git a/t/t7001-mv.sh b/t/t7001-mv.sh index 322eaadc73..900ca93cde 100755 --- a/t/t7001-mv.sh +++ b/t/t7001-mv.sh @@ -74,4 +74,8 @@ test_expect_success \ git-diff-tree -r -M --name-status HEAD^ HEAD | \ grep -E "^R100.+path2/README.+path1/path2/README"' +test_expect_failure \ + 'do not move directory over existing directory' \ + 'mkdir path0 && mkdir path0/path2 && git-mv path2 path0' + test_done