From: "Lucas Zamboni Orioli via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Ben Knoble <ben.knoble@gmail.com>,
Pablo Sabater <pabloosabaterr@gmail.com>,
Junio C Hamano <gitster@pobox.com>,
Lucas Zamboni Orioli <lucaszam0@gmail.com>
Subject: [PATCH v5 0/2] mv: report missing destination leading directory
Date: Thu, 30 Jul 2026 11:28:02 +0000 [thread overview]
Message-ID: <pull.2356.v5.git.git.1785410884.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2356.v4.git.git.1785097071.gitgitgadget@gmail.com>
Changes in v5:
* extracted the shared "will this move rename on disk?" condition into a
needs_worktree_rename() helper used by both the new leading-directory
check and the actual rename(), so the two cannot drift, per Junio C
Hamano
* allocate the dirname copy only when the destination has a slash
* reworded the opening of the commit message for clarity, per Junio C
Hamano
* added tests: moving into an existing directory (destination is normalized
to a full path), and moving to a bare filename in the cwd (no leading
directory to check)
Changes in v4:
* reverted to lstat and added has_symlink_leading_path() to refuse a
destination that goes through a symbolic link, independent of the link
target, per Junio C Hamano's point that Git tracks symlinks and must not
follow them here
* added new "destination is beyond a symbolic link" message
* added tests: symlink as immediate parent and as intermediate component,
symlink at the destination, -f does not bypass the symlink refusal, and a
regression test that a move through a symlink no longer corrupts the
index (see the reproduction reported on the list)
Changes in v3:
* added ENOTDIR handling and an S_ISDIR check so a non-directory leading
path component is caught, as suggested by Junio C Hamano
* (v3 used stat() to resolve symlinks; this was reverted in v4 after Junio
pointed out symlinks must not be followed)
* fixed indentation
Changes in v2:
* altered the error message to include both source and destination as
suggested by Ben Knoble
Lucas Zamboni Orioli (2):
mv: name both source and destination when rename fails
mv: reject a destination whose leading path is missing or a symlink
builtin/mv.c | 47 +++++++++++++++++++++--
t/t7001-mv.sh | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 146 insertions(+), 3 deletions(-)
base-commit: 9a0c4701dcd5725c4184599322b52933ff5005ca
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2356%2FZamboniL%2Fmv-detect-non-existing-target-folder-v5
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2356/ZamboniL/mv-detect-non-existing-target-folder-v5
Pull-Request: https://github.com/git/git/pull/2356
Range-diff vs v4:
1: 0d67da588b = 1: 0d67da588b mv: name both source and destination when rename fails
2: 6b72efb413 ! 2: 6c2909e609 mv: reject a destination whose leading path is missing or a symlink
@@ Metadata
## Commit message ##
mv: reject a destination whose leading path is missing or a symlink
- Moving a file into a destination whose leading directories are not all
- present, real directories is only diagnosed later at rename(2), and for
- a symlinked component is not diagnosed at all.
+ 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:
@@ builtin/mv.c
#include "setup.h"
#include "strvec.h"
+@@ builtin/mv.c: enum update_mode {
+ 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
+
@@ builtin/mv.c: dir_check:
bad = _("destination directory does not exist");
goto act_on_entry;
@@ builtin/mv.c: dir_check:
+ * If we are going to move SRC to DST on disk, DST's leading
+ * directories must already exist.
+ */
-+ if (!(modes[i] & (INDEX | SPARSE | SKIP_WORKTREE_DIR)) &&
-+ !(dst_mode & (SKIP_WORKTREE_DIR | SPARSE))) {
-+ char *dst_dir = xstrdup(dst);
-+ char *slash = strrchr(dst_dir, '/');
++ if (needs_worktree_rename(modes[i], dst_mode)) {
++ const char *slash_ = strrchr(dst, '/');
+
-+ if (slash) {
++ 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) {
@@ builtin/mv.c: dir_check:
+ } else if (!S_ISDIR(dir_st.st_mode)) {
+ bad = _("destination is not a directory");
+ }
++
++ free(dst_dir);
+ }
-+ free(dst_dir);
+
+ if (bad)
+ goto act_on_entry;
@@ builtin/mv.c: dir_check:
if (ignore_sparse &&
(dst_mode & (SKIP_WORKTREE_DIR | SPARSE)) &&
+@@ builtin/mv.c: remove_entry:
+ 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;
## t/t7001-mv.sh ##
@@ t/t7001-mv.sh: test_expect_success 'clean up' '
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 &&
--
gitgitgadget
next prev parent reply other threads:[~2026-07-30 11:28 UTC|newest]
Thread overview: 27+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-15 14:32 [PATCH] mv: report missing destination leading directory Lucas Zamboni Orioli via GitGitGadget
2026-07-15 16:46 ` Ben Knoble
2026-07-22 21:32 ` Lucas Zamboni Orioli
2026-07-23 13:13 ` [PATCH v2 0/2] " Lucas Zamboni Orioli via GitGitGadget
2026-07-23 13:13 ` [PATCH v2 1/2] mv: name both source and destination when rename fails Lucas Zamboni Orioli via GitGitGadget
2026-07-23 17:36 ` Junio C Hamano
2026-07-23 13:13 ` [PATCH v2 2/2] mv: check for missing destination directory before renaming Lucas Zamboni Orioli via GitGitGadget
2026-07-23 17:42 ` Junio C Hamano
2026-07-23 18:30 ` Junio C Hamano
2026-07-23 21:38 ` Lucas Zamboni Orioli
2026-07-23 22:40 ` Junio C Hamano
2026-07-23 23:28 ` Junio C Hamano
2026-07-26 14:59 ` Junio C Hamano
2026-07-26 17:59 ` Lucas Zamboni Orioli
2026-07-23 21:40 ` [PATCH v3 0/2] mv: report missing destination leading directory Lucas Zamboni Orioli via GitGitGadget
2026-07-23 21:40 ` [PATCH v3 1/2] mv: name both source and destination when rename fails Lucas Zamboni Orioli via GitGitGadget
2026-07-23 21:40 ` [PATCH v3 2/2] mv: check for missing destination directory before renaming Lucas Zamboni Orioli via GitGitGadget
2026-07-26 15:28 ` Pablo Sabater
2026-07-26 20:17 ` [PATCH v4 0/2] mv: report missing destination leading directory Lucas Zamboni Orioli via GitGitGadget
2026-07-26 20:17 ` [PATCH v4 1/2] mv: name both source and destination when rename fails Lucas Zamboni Orioli via GitGitGadget
2026-07-26 20:17 ` [PATCH v4 2/2] mv: reject a destination whose leading path is missing or a symlink Lucas Zamboni Orioli via GitGitGadget
2026-07-27 22:24 ` Junio C Hamano
2026-07-30 11:23 ` Lucas Zamboni Orioli
2026-07-26 23:29 ` [PATCH v4 0/2] mv: report missing destination leading directory Junio C Hamano
2026-07-30 11:28 ` Lucas Zamboni Orioli via GitGitGadget [this message]
2026-07-30 11:28 ` [PATCH v5 1/2] mv: name both source and destination when rename fails Lucas Zamboni Orioli via GitGitGadget
2026-07-30 11:28 ` [PATCH v5 2/2] mv: reject a destination whose leading path is missing or a symlink Lucas Zamboni Orioli via GitGitGadget
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=pull.2356.v5.git.git.1785410884.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=ben.knoble@gmail.com \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=lucaszam0@gmail.com \
--cc=pabloosabaterr@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox