All of lore.kernel.org
 help / color / mirror / Atom feed
From: "Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: "Eric Sunshine" <sunshine@sunshineco.com>,
	"Johannes Schindelin" <johannes.schindelin@gmx.de>,
	"Thomas Bétous" <tomspycell@gmail.com>
Subject: [PATCH v2] mingw: align symlinks-related rmdir() behavior with Linux
Date: Mon, 02 Aug 2021 21:07:30 +0000	[thread overview]
Message-ID: <pull.1008.v2.git.1627938451247.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.1008.git.1627586493659.gitgitgadget@gmail.com>

From: =?UTF-8?q?Thomas=20B=C3=A9tous?= <tomspycell@gmail.com>

When performing a rebase, rmdir() is called on the folder .git/logs. On
Unix rmdir() exits without deleting anything in case .git/logs is a
symbolic link but the equivalent functions on Windows (_rmdir, _wrmdir
and RemoveDirectoryW) do not behave the same and remove the folder if it
is symlinked even if it is not empty.

This creates issues when folders in .git/ are symlinks which is
especially the case when git-repo[1] is used: It replaces `.git/logs/`
with a symlink.

One such issue is that the _target_ of that symlink is removed e.g.
during a `git rebase`, where `delete_reflog("REBASE_HEAD")` will not
only try to remove `.git/logs/REBASE_HEAD` but then recursively try to
remove the parent directories until an error occurs, a technique that
obviously relies on `rmdir()` refusing to remove a symlink.

This was reported in https://github.com/git-for-windows/git/issues/2967.

This commit updates mingw_rmdir() so that its behavior is the same as
Linux rmdir() in case of symbolic links.

To verify that Git does not regress on the reported issue, this patch
adds a regression test for the `git rebase` symptom, even if the same
`rmdir()` behavior is quite likely to cause potential problems in other
Git commands as well.

[1]: git-repo is a python tool built on top of Git which helps manage
many Git repositories. It stores all the .git/ folders in a central
place by taking advantage of symbolic links.
More information: https://gerrit.googlesource.com/git-repo/

Signed-off-by: Thomas Bétous <tomspycell@gmail.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
    mingw: support the git-repo tool better
    
    This addresses an issue, originally reported at
    https://github.com/git-for-windows/git/issues/2967, where the git-repo
    tool [https://gerrit.googlesource.com/git-repo/] replaces folders in
    .git/ with symlinks and mingw_rmdir() erroneously removes the symlink
    target directory's contents.
    
    Changes since v1:
    
     * Fixed the authorship
     * Augmented the commit message to elaborate on what the issues are,
       concretely, with the current behavior of mingw_rmdir()
     * Added an explanation to the commit message as to what happens during
       a git rebase that would trigger the rmdir() code path to misbehave
     * Adjusted the code comment to talk specifically about the
       remove_path() caller and why we want to align with Linux' rmdir()
       behavior

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1008%2Fdscho%2Ffix-rmdir-with-symlinks-on-windows-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1008/dscho/fix-rmdir-with-symlinks-on-windows-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/1008

Range-diff vs v1:

 1:  c5a7fe0007f ! 1:  0a9cb1bcc14 mingw: align symlinks-related rmdir() behavior with Linux
     @@
       ## Metadata ##
     -Author: Johannes Schindelin <Johannes.Schindelin@gmx.de>
     +Author: Thomas Bétous <tomspycell@gmail.com>
      
       ## Commit message ##
          mingw: align symlinks-related rmdir() behavior with Linux
     @@ Commit message
          is symlinked even if it is not empty.
      
          This creates issues when folders in .git/ are symlinks which is
     -    especially the case when git-repo[1] is used.
     +    especially the case when git-repo[1] is used: It replaces `.git/logs/`
     +    with a symlink.
     +
     +    One such issue is that the _target_ of that symlink is removed e.g.
     +    during a `git rebase`, where `delete_reflog("REBASE_HEAD")` will not
     +    only try to remove `.git/logs/REBASE_HEAD` but then recursively try to
     +    remove the parent directories until an error occurs, a technique that
     +    obviously relies on `rmdir()` refusing to remove a symlink.
     +
     +    This was reported in https://github.com/git-for-windows/git/issues/2967.
      
          This commit updates mingw_rmdir() so that its behavior is the same as
          Linux rmdir() in case of symbolic links.
      
     -    This fixes https://github.com/git-for-windows/git/issues/2967
     +    To verify that Git does not regress on the reported issue, this patch
     +    adds a regression test for the `git rebase` symptom, even if the same
     +    `rmdir()` behavior is quite likely to cause potential problems in other
     +    Git commands as well.
      
          [1]: git-repo is a python tool built on top of Git which helps manage
          many Git repositories. It stores all the .git/ folders in a central
     @@ compat/mingw.c: int mingw_rmdir(const char *pathname)
      +	struct stat st;
      +
      +	/*
     -+	* Contrary to Linux rmdir(), Windows' _wrmdir() and _rmdir()
     -+	* will remove the directory at the path if it is a symbolic link
     -+	* which leads to issues when symlinks are used in the .git folder
     -+	* (in the context of git-repo for instance). So before calling _wrmdir()
     -+	* we first check if the path is a symbolic link. If it is, we exit
     -+	* and return the same error as Linux rmdir() in this case (ENOTDIR).
     -+	*/
     ++	 * Contrary to Linux' `rmdir()`, Windows' _wrmdir() and _rmdir()
     ++	 * (and `RemoveDirectoryW()`) will attempt to remove the target of a
     ++	 * symbolic link (if it points to a directory).
     ++	 *
     ++	 * This behavior breaks the assumption of e.g. `remove_path()` which
     ++	 * upon successful deletion of a file will attempt to remove its parent
     ++	 * directories recursively until failure (which usually happens when
     ++	 * the directory is not empty).
     ++	 *
     ++	 * Therefore, before calling `_wrmdir()`, we first check if the path is
     ++	 * a symbolic link. If it is, we exit and return the same error as
     ++	 * Linux' `rmdir()` would, i.e. `ENOTDIR`.
     ++	 */
      +	if (!mingw_lstat(pathname, &st) && S_ISLNK(st.st_mode)) {
      +		errno = ENOTDIR;
      +		return -1;


 compat/mingw.c    | 21 +++++++++++++++++++++
 t/t3400-rebase.sh | 10 ++++++++++
 t/test-lib.sh     |  6 ++++++
 3 files changed, 37 insertions(+)

diff --git a/compat/mingw.c b/compat/mingw.c
index aa647b367b0..9e0cd1e097f 100644
--- a/compat/mingw.c
+++ b/compat/mingw.c
@@ -341,6 +341,27 @@ int mingw_rmdir(const char *pathname)
 {
 	int ret, tries = 0;
 	wchar_t wpathname[MAX_PATH];
+	struct stat st;
+
+	/*
+	 * Contrary to Linux' `rmdir()`, Windows' _wrmdir() and _rmdir()
+	 * (and `RemoveDirectoryW()`) will attempt to remove the target of a
+	 * symbolic link (if it points to a directory).
+	 *
+	 * This behavior breaks the assumption of e.g. `remove_path()` which
+	 * upon successful deletion of a file will attempt to remove its parent
+	 * directories recursively until failure (which usually happens when
+	 * the directory is not empty).
+	 *
+	 * Therefore, before calling `_wrmdir()`, we first check if the path is
+	 * a symbolic link. If it is, we exit and return the same error as
+	 * Linux' `rmdir()` would, i.e. `ENOTDIR`.
+	 */
+	if (!mingw_lstat(pathname, &st) && S_ISLNK(st.st_mode)) {
+		errno = ENOTDIR;
+		return -1;
+	}
+
 	if (xutftowcs_path(wpathname, pathname) < 0)
 		return -1;
 
diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh
index 0bb88aa982b..23dbd3c82ed 100755
--- a/t/t3400-rebase.sh
+++ b/t/t3400-rebase.sh
@@ -406,4 +406,14 @@ test_expect_success 'refuse to switch to branch checked out elsewhere' '
 	test_i18ngrep "already checked out" err
 '
 
+test_expect_success MINGW,SYMLINKS_WINDOWS 'rebase when .git/logs is a symlink' '
+	git checkout main &&
+	mv .git/logs actual_logs &&
+	cmd //c "mklink /D .git\logs ..\actual_logs" &&
+	git rebase -f HEAD^ &&
+	test -L .git/logs &&
+	rm .git/logs &&
+	mv actual_logs .git/logs
+'
+
 test_done
diff --git a/t/test-lib.sh b/t/test-lib.sh
index adaf03543e8..73f6d645b66 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -1513,6 +1513,12 @@ test_lazy_prereq SYMLINKS '
 	ln -s x y && test -h y
 '
 
+test_lazy_prereq SYMLINKS_WINDOWS '
+	# test whether symbolic links are enabled on Windows
+	test_have_prereq MINGW &&
+	cmd //c "mklink y x" &> /dev/null && test -h y
+'
+
 test_lazy_prereq FILEMODE '
 	test "$(git config --bool core.filemode)" = true
 '

base-commit: ebf3c04b262aa27fbb97f8a0156c2347fecafafb
-- 
gitgitgadget

      parent reply	other threads:[~2021-08-02 21:07 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-07-29 19:21 [PATCH] mingw: align symlinks-related rmdir() behavior with Linux Johannes Schindelin via GitGitGadget
2021-07-29 19:23 ` Johannes Schindelin
2021-07-29 20:03 ` Junio C Hamano
2021-08-02 20:17   ` Johannes Schindelin
2021-07-29 20:12 ` Eric Sunshine
2021-08-02 20:28   ` Johannes Schindelin
2021-08-02 21:07 ` Johannes Schindelin via GitGitGadget [this message]

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.1008.v2.git.1627938451247.gitgitgadget@gmail.com \
    --to=gitgitgadget@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=johannes.schindelin@gmx.de \
    --cc=sunshine@sunshineco.com \
    --cc=tomspycell@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.