mv: reject a destination whose leading path is missing or a symlink
When moving a file, if any leading directory in the destination path is missing or is not a real directory, the problem is detected only later when rename() is called. Furthermore, if a leading directory component is a symbolic link, the issue is not detected at all. Three cases reach rename(2) unchecked today: - A leading directory is missing: rename(2) fails with ENOENT, reported against the source (misleading), and "git mv -n" does not detect it since the dry run never reaches the syscall. - A leading component is a non-directory ("git mv x a/b" with 'a' a file): rename(2) fails with ENOTDIR, again only at the syscall. - A leading component is a symbolic link: "git mv" follows it. Since Git tracks symlinks, the destination is really occupied by a tracked object, and following it is wrong regardless of the link target. The move is done on disk at the resolved location while the index records the literal path, leaving the index describing a worktree that does not exist. A later "git add" can reconcile it, but "git mv" alone has already corrupted the state. Detect all three in the checking phase. Reject a destination that goes through a symlink with has_symlink_leading_path(), which uses lstat() and never follows the link, so the refusal is independent of the target. Then lstat() the leading directory: report "destination directory does not exist" for ENOENT/ENOTDIR and "destination is not a directory" for a non-directory. Other errors fall through to rename(). Guard the directory check with the same condition under which rename(2) runs, so directory moves and sparse/out-of-cone destinations are not flagged incorrectly. This changes behavior: a move through a tracked symlink that previously "succeeded" while corrupting the index is now refused. The other two cases only change when the failure is diagnosed. Signed-off-by: Lucas Zamboni Orioli <lucaszam0@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>