mv: let 'git mv file no-such-dir/' error out

Git used to trim the trailing slash, and make the command equivalent to 'git mv file no-such-dir', which created the file no-such-dir (while the trailing slash explicitly stated that it could only be a directory). This patch skips the trailing slash removal for the destination path. The path with its trailing slash is passed to rename(2), which errors out with the appropriate message: $ git mv file no-such-dir/ fatal: renaming 'file' failed: Not a directory Original-patch-by: Duy Nguyen <pclouds@gmail.com> Signed-off-by: Matthieu Moy <Matthieu.Moy@imag.fr> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Matthieu Moy committed Dec 3, 2013 at 09:32 UTC c57f6281ff847db7094d708f5c5b939d378f7120
2 files changed +45 -7
builtin/mv.c
+16 -7
@@ -16,9 +16,12 @@ static const char * const builtin_mv_usage[] = {
16 NULL
17 };
18
19 +#define DUP_BASENAME 1
20 +#define KEEP_TRAILING_SLASH 2
21 +
22 static const char **internal_copy_pathspec(const char *prefix,
23 const char **pathspec,
21 - int count, int base_name)
24 + int count, unsigned flags)
25 {
26 int i;
27 const char **result = xmalloc((count + 1) * sizeof(const char *));
@@ -27,11 +30,12 @@ static const char **internal_copy_pathspec(const char *prefix,
30 for (i = 0; i < count; i++) {
31 int length = strlen(result[i]);
32 int to_copy = length;
30 - while (to_copy > 0 && is_dir_sep(result[i][to_copy - 1]))
33 + while (!(flags & KEEP_TRAILING_SLASH) &&
34 + to_copy > 0 && is_dir_sep(result[i][to_copy - 1]))
35 to_copy--;
32 - if (to_copy != length || base_name) {
36 + if (to_copy != length || flags & DUP_BASENAME) {
37 char *it = xmemdupz(result[i], to_copy);
34 - if (base_name) {
38 + if (flags & DUP_BASENAME) {
39 result[i] = xstrdup(basename(it));
40 free(it);
41 } else
@@ -87,16 +91,21 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
91
92 source = internal_copy_pathspec(prefix, argv, argc, 0);
93 modes = xcalloc(argc, sizeof(enum update_mode));
90 - dest_path = internal_copy_pathspec(prefix, argv + argc, 1, 0);
94 + /*
95 + * Keep trailing slash, needed to let
96 + * "git mv file no-such-dir/" error out.
97 + */
98 + dest_path = internal_copy_pathspec(prefix, argv + argc, 1,
99 + KEEP_TRAILING_SLASH);
100 submodule_gitfile = xcalloc(argc, sizeof(char *));
101
102 if (dest_path[0][0] == '\0')
103 /* special case: "." was normalized to "" */
95 - destination = internal_copy_pathspec(dest_path[0], argv, argc, 1);
104 + destination = internal_copy_pathspec(dest_path[0], argv, argc, DUP_BASENAME);
105 else if (!lstat(dest_path[0], &st) &&
106 S_ISDIR(st.st_mode)) {
107 dest_path[0] = add_slash(dest_path[0]);
99 - destination = internal_copy_pathspec(dest_path[0], argv, argc, 1);
108 + destination = internal_copy_pathspec(dest_path[0], argv, argc, DUP_BASENAME);
109 } else {
110 if (argc != 1)
111 die("destination '%s' is not a directory", dest_path[0]);
t/t7001-mv.sh
+29
@@ -70,6 +70,35 @@ test_expect_success \
70 rm -f idontexist untracked1 untracked2 \
71 path0/idontexist path0/untracked1 path0/untracked2 \
72 .git/index.lock
73 +rmdir path1
74 +
75 +test_expect_success \
76 + 'moving to absent target with trailing slash' \
77 + 'test_must_fail git mv path0/COPYING no-such-dir/ &&
78 + test_must_fail git mv path0/COPYING no-such-dir// &&
79 + git mv path0/ no-such-dir/ &&
80 + test_path_is_dir no-such-dir'
81 +
82 +test_expect_success \
83 + 'clean up' \
84 + 'git reset --hard'
85 +
86 +test_expect_success \
87 + 'moving to existing untracked target with trailing slash' \
88 + 'mkdir path1 &&
89 + git mv path0/ path1/ &&
90 + test_path_is_dir path1/path0/'
91 +
92 +test_expect_success \
93 + 'moving to existing tracked target with trailing slash' \
94 + 'mkdir path2 &&
95 + >path2/file && git add path2/file &&
96 + git mv path1/path0/ path2/ &&
97 + test_path_is_dir path2/path0/'
98 +
99 +test_expect_success \
100 + 'clean up' \
101 + 'git reset --hard'
102
103 test_expect_success \
104 'adding another file' \