All of lore.kernel.org
 help / color / mirror / Atom feed
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

  parent reply	other threads:[~2026-07-30 11:28 UTC|newest]

Thread overview: 28+ 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
2026-07-30 20:13         ` [PATCH v5 0/2] mv: report missing destination leading directory Junio C Hamano

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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.