Description
Summary
Running git mv symlink symlink-renamed
where symlink
is a symbolic link that points to file
is supposed to rename symlink
to symlink-renamed
and leave file
alone. But starting in Git for Windows 2.48.1, it instead leaves symlink
unchanged and renames file
to symlink-renamed
. This only happens on Windows. I have verified this happens on Windows 10 and Windows 11 and that it does not depend on how Git for Windows is installed.
Fortunately, this only affects operations where rename()
is called. This is not limited to git mv
, but that seems to be the only operation where it is likely to occur on files in a working tree. As far as I can tell, both through experimentation and by examining the code and searching for uses of rename
, this is not triggered by operations such as switching branches.
The cause
The problem happens because of the new mingw_rename()
implementation merged in 183ea3e, which needs FILE_FLAG_OPEN_REPARSE_POINT
as one of the flags in the dwFlagsAndAttributes
argument in order for CreateFileW
to open the symlink rather than the target file, but omits that flag:
Lines 2927 to 2929 in 2bd190b
Verification and proposed fix
I have verified that this change to compat/mingw.c
fixes the regression:
@@ -2926,7 +2926,9 @@ int mingw_rename(const char *pold, const char *pnew)
old_handle = CreateFileW(wpold, DELETE,
FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE,
- NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
+ NULL, OPEN_EXISTING,
+ FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
+ NULL);
if (old_handle == INVALID_HANDLE_VALUE) {
errno = err_win_to_posix(GetLastError());
return -1;
The fix is very simple, so I'm not sure how useful it would be for me to submit a patch, but I'll open a pull request offering a commit with the change with what I hope to be a suitable commit message.
(Ordinarily a patch would include new tests, but I am unsure if that is needed here. As detailed below, this regression does cause existing tests to fail when they are run in a way that permits the user, git
, and ln
to create symlinks--and I suspect any new tests would overlap significantly with existing ones and have similar preconditions.)
Steps to reproduce
I first observed this this on Windows 10 in a 64-bit git
installation managed by scoop
, initially when running test fixture script in the gitoxide test suite that use git
(GitoxideLabs/gitoxide#1849), then later interactively with the simplified instructions given below (not involving gitoxide or any extra complexity).
To ensure my environment was not the cause, I tested it on Windows 11 with 64-bit PortableGit, also with versions 2.47.1(2) and 2.48.1, verifying that only 2.48.1 was affected. Then I tested it again on both the Windows 10 and Windows 11 systems with the Git for Windows SDK to compare the version installed by sdk cd git
(which is currently the same commit as 2.48.1) and the ameliorating effect of adding the FILE_FLAG_OPEN_REPARSE_POINT
flag as shown in the above diff.
On both systems, I enabled Developer Mode to allow all users to create symlinks (and to do so without UAC elevation), and set core.symlinks
to true
via git config --global core.symlinks true
.
Because Windows distinguishes file and directory symlinks, as well as to ensure my proposed fix does not break the improvements of b30404d, I tested with two repositories: one with a symlink to a regular file, and another with a symlink to a directory. The following is from PortableGit 2.48.1 on the Windows 11 system, in Git Bash, testing with the simple has-symlink
repository:
ek@DESKTOP-0L1QG1Q MINGW64 ~/src
$ git clone https://github.com/EliahKagan/has-symlink.git Cloning into 'has-symlink'...
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 8 (delta 0), reused 8 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (8/8), done.
ek@DESKTOP-0L1QG1Q MINGW64 ~/src
$ cd has-symlink/
ek@DESKTOP-0L1QG1Q MINGW64 ~/src/has-symlink (main)
$ ls -l
total 3
-rw-r--r-- 1 ek 197121 617 Feb 21 00:42 COPYING
-rw-r--r-- 1 ek 197121 200 Feb 21 00:42 README.md
lrwxrwxrwx 1 ek 197121 6 Feb 21 00:42 symlink -> target
-rw-r--r-- 1 ek 197121 25 Feb 21 00:42 target
ek@DESKTOP-0L1QG1Q MINGW64 ~/src/has-symlink (main)
$ cmd //c dir
Volume in drive C has no label.
Volume Serial Number is E039-3F46
Directory of C:\Users\ek\src\has-symlink
02/21/2025 12:42 AM <DIR> .
02/21/2025 12:42 AM <DIR> ..
02/21/2025 12:42 AM 617 COPYING
02/21/2025 12:42 AM 200 README.md
02/21/2025 12:42 AM <SYMLINK> symlink [target]
02/21/2025 12:42 AM 25 target
4 File(s) 842 bytes
2 Dir(s) 220,282,855,424 bytes free
ek@DESKTOP-0L1QG1Q MINGW64 ~/src/has-symlink (main)
$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
ek@DESKTOP-0L1QG1Q MINGW64 ~/src/has-symlink (main)
$ git mv symlink symlink-renamed
ek@DESKTOP-0L1QG1Q MINGW64 ~/src/has-symlink (main)
$ ls -l
total 3
-rw-r--r-- 1 ek 197121 617 Feb 21 00:42 COPYING
-rw-r--r-- 1 ek 197121 200 Feb 21 00:42 README.md
lrwxrwxrwx 1 ek 197121 6 Feb 21 00:42 symlink -> target
-rw-r--r-- 1 ek 197121 25 Feb 21 00:42 symlink-renamed
ek@DESKTOP-0L1QG1Q MINGW64 ~/src/has-symlink (main)
$ cmd //c dir
Volume in drive C has no label.
Volume Serial Number is E039-3F46
Directory of C:\Users\ek\src\has-symlink
02/21/2025 12:43 AM <DIR> .
02/21/2025 12:42 AM <DIR> ..
02/21/2025 12:42 AM 617 COPYING
02/21/2025 12:42 AM 200 README.md
02/21/2025 12:42 AM <SYMLINK> symlink [target]
02/21/2025 12:42 AM 25 symlink-renamed
4 File(s) 842 bytes
2 Dir(s) 220,282,855,424 bytes free
ek@DESKTOP-0L1QG1Q MINGW64 ~/src/has-symlink (main)
$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
renamed: symlink -> symlink-renamed
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
typechange: symlink-renamed
deleted: target
Untracked files:
(use "git add <file>..." to include in what will be committed)
symlink
I observed the same effect on a repository with a directory symlink, has-symlink-to-directory
. All tested versions, including the affected version 2.48.1, cloned these repositories without problems and checked out the symlinks. Only renaming/moving is affected.
Reproduction via the test suite
Provided the user who runs the t7001-mv.sh
tests has the ability to create symlinks (and it is not filtered out via UAC), and provided MSYS
is set to a value that causes ln -s
to create actual symlinks (such as winsymlinks:nativestrict
), two of the tests in that file fail when the regression is present, then pass under the change described above:
not ok 38 - git mv should overwrite file with a symlink
#
# rm -fr .git &&
# git init &&
# echo 1 >moved &&
# test_ln_s_add moved symlink &&
# git add moved &&
# test_must_fail git mv symlink moved &&
# git mv -f symlink moved &&
# test_path_is_missing symlink &&
# git update-index --refresh &&
# git diff-files --quiet
#
not ok 39 - check moved symlink
#
# test_path_is_symlink moved
#
System details
The usual requested information about the environment is as follows, for the Windows 11 system and running the commands in Git Bash provided by PortableGit 2.48.1:
$ git --version --build-options
git version 2.48.1.windows.1
cpu: x86_64
built from commit: 2bd190bcd280bc95f537c2d532880a5e539b5132
sizeof-long: 4
sizeof-size_t: 8
shell-path: D:/git-sdk-64-build-installers/usr/bin/sh
feature: fsmonitor--daemon
libcurl: 8.12.1
OpenSSL: OpenSSL 3.2.4 11 Feb 2025
zlib: 1.3.1
$ cmd //c ver
Microsoft Windows [Version 10.0.26100.3194]
While /etc/install-options.txt
doesn't seem to be available anymore, some of that information, as well as other details, are shown by this git config
command:
$ git config -l --show-scope
system core.symlinks=false
system core.autocrlf=true
system core.fscache=true
system color.interactive=true
system color.ui=auto
system help.format=html
system diff.astextplain.textconv=astextplain
system rebase.autosquash=true
system filter.lfs.clean=git-lfs clean -- %f
system filter.lfs.smudge=git-lfs smudge -- %f
system filter.lfs.process=git-lfs filter-process
system filter.lfs.required=true
system credential.helper=helper-selector
global core.symlinks=true