#include "string-list.h"
#include "parse-options.h"
#include "read-cache-ll.h"
+#include "symlinks.h"
#include "setup.h"
#include "strvec.h"
MOVE_VIA_PARENT_DIR = (1 << 5),
};
+static int needs_worktree_rename(enum update_mode mode, enum update_mode dst_mode)
+{
+ return !(mode & (INDEX | SPARSE | SKIP_WORKTREE_DIR)) &&
+ !(dst_mode & (SKIP_WORKTREE_DIR | SPARSE));
+}
+
#define DUP_BASENAME 1
#define KEEP_TRAILING_SLASH 2
bad = _("destination directory does not exist");
goto act_on_entry;
}
+ if (has_symlink_leading_path(dst, strlen(dst))) {
+ bad = _("destination is beyond a symbolic link");
+ goto act_on_entry;
+ }
+
+ /*
+ * If we are going to move SRC to DST on disk, DST's leading
+ * directories must already exist.
+ */
+ if (needs_worktree_rename(modes[i], dst_mode)) {
+ const char *slash_ = strrchr(dst, '/');
+
+ if (slash_) {
+ struct stat dir_st;
+ char *dst_dir = xstrdup(dst);
+ char *slash = &dst_dir[slash_ - dst];
+
+ *slash = '\0';
+ if (lstat(dst_dir, &dir_st) < 0) {
+ /*
+ * other errors fall through to rename(),
+ * which reports them
+ */
+ if (errno == ENOENT || errno == ENOTDIR)
+ bad = _("destination directory does not exist");
+ } else if (!S_ISDIR(dir_st.st_mode)) {
+ bad = _("destination is not a directory");
+ }
+
+ free(dst_dir);
+ }
+
+ if (bad)
+ goto act_on_entry;
+ }
if (ignore_sparse &&
(dst_mode & (SKIP_WORKTREE_DIR | SPARSE)) &&
printf(_("Renaming %s to %s\n"), src, dst);
if (show_only)
continue;
- if (!(mode & (INDEX | SPARSE | SKIP_WORKTREE_DIR)) &&
- !(dst_mode & (SKIP_WORKTREE_DIR | SPARSE)) &&
+ if (needs_worktree_rename(mode, dst_mode) &&
rename(src, dst) < 0) {
if (ignore_errors)
continue;
git reset --hard
'
+test_expect_success 'moving file to directory without trailing slash' '
+ git reset --hard HEAD &&
+ rm -rf file.txt target && mkdir target &&
+ echo content > file.txt &&
+ git add file.txt &&
+ git mv file.txt target &&
+ test_path_is_file target/file.txt
+'
+
+test_expect_success 'moving file to a bare filename in the cwd' '
+ git reset --hard &&
+ rm -rf from dest.txt &&
+ mkdir from &&
+ echo content >from/file &&
+ git add from/file &&
+ git mv from/file dest.txt &&
+ test_path_is_file dest.txt
+'
+
+test_expect_success 'moving to a non-existent directory' '
+ git reset --hard &&
+ rm -rf from && mkdir from &&
+ echo content >from/file &&
+ git add from/file &&
+ test_must_fail git mv from/file no-such-dir/file 2>actual &&
+ test_grep "destination directory does not exist" actual
+'
+
+test_expect_success 'moving to a destination with a file as a leading path component' '
+ git reset --hard &&
+ rm -rf from && mkdir from &&
+ echo contents >from/file &&
+ echo blocker >not-dir &&
+ git add from/file &&
+ test_must_fail git mv from/file not-dir/file 2>actual &&
+ test_grep "destination is not a directory" actual
+'
+
+test_expect_success SYMLINKS 'moving to a destination beyond a symlink' '
+ git reset --hard &&
+ rm -rf from regular-dir link-to-dir &&
+ mkdir from regular-dir &&
+ echo contents >from/file &&
+ ln -s regular-dir link-to-dir &&
+ git add from/file &&
+ test_must_fail git mv from/file link-to-dir/file 2>actual &&
+ test_grep "destination is beyond a symbolic link" actual
+'
+
+test_expect_success SYMLINKS 'moving to a destination with a symlink as an intermediate component' '
+ git reset --hard &&
+ rm -rf from && mkdir -p from/real/inner &&
+ echo contents >from/file &&
+ ln -s real from/link &&
+ git add from/file from/link &&
+ test_must_fail git mv from/file from/link/inner/dst 2>actual &&
+ test_grep "destination is beyond a symbolic link" actual
+'
+
+test_expect_success SYMLINKS 'refuses to overwrite a symlink at the destination' '
+ git reset --hard &&
+ rm -rf from && mkdir from &&
+ echo contents >from/file &&
+ ln -s target from/link &&
+ git add from/file from/link &&
+ test_must_fail git mv from/file from/link 2>actual &&
+ test_grep "destination exists" actual
+'
+
+test_expect_success SYMLINKS 'mv through a symlinked leading path does not touch the index' '
+ git reset --hard &&
+ rm -rf from && mkdir from &&
+ echo contents >from/src &&
+ ln -s . from/link &&
+ git add from/src from/link &&
+ git commit -m "setup symlink case" &&
+ git ls-files --stage >expect.index &&
+ test_must_fail git mv from/src from/link/real/dst 2>actual &&
+ test_grep "destination is beyond a symbolic link" actual &&
+ git ls-files --stage >actual.index &&
+ test_cmp expect.index actual.index
+'
+
+test_expect_success SYMLINKS 'mv -f does not follow a symlinked leading path' '
+ git reset --hard &&
+ rm -rf from && mkdir from &&
+ echo contents >from/src &&
+ ln -s file from/link &&
+ git add from/src from/link &&
+ test_must_fail git mv -f from/src from/link/dst 2>actual &&
+ test_grep "destination is beyond a symbolic link" actual
+'
+
+test_expect_success 'mv --dry-run detects non-existent destination parent directory' '
+ git reset --hard &&
+ rm -rf from && mkdir from &&
+ echo contents >from/file &&
+ git add from/file &&
+ test_must_fail git mv -n from/file no-such-dir/file 2>actual &&
+ test_grep "destination directory does not exist" actual
+'
+
test_expect_success 'moving to existing untracked target with trailing slash' '
mkdir path1 &&
git mv path0/ path1/ &&