Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9693157
t9700: accommodate for Windows paths
dscho Dec 16, 2025
33ccd0f
apply: symbolic links lack a "trustable executable bit"
dscho Dec 16, 2025
78d52c8
mingw: special-case `open(symlink, O_CREAT | O_EXCL)`
dscho Dec 16, 2025
c8fb79c
t0001: handle `diff --no-index` gracefully
dscho Dec 16, 2025
d2de5d9
t0301: another fix for Windows compatibility
dscho Dec 16, 2025
14b033f
t0600: fix incomplete prerequisite for a test case
dscho Dec 5, 2025
c670ae3
t1006: accommodate for symlink support in MSYS2
dscho Dec 16, 2025
d265cde
t1305: skip symlink tests that do not apply to Windows
dscho Dec 16, 2025
02ea262
t6423: introduce Windows-specific handling for symlinking to /dev/null
dscho Dec 16, 2025
a4c7170
t7800: work around the MSYS path conversion on Windows
dscho Dec 16, 2025
359de97
Merge branch 'js/test-symlink-windows' into js/prep-symlink-windows
gitster Dec 16, 2025
df27101
mingw: do resolve symlinks in `getcwd()`
dscho Dec 16, 2025
722e65d
init: do parse _all_ core.* settings early
dscho Dec 16, 2025
f6b8e4a
strbuf_readlink(): avoid calling `readlink()` twice in corner-cases
kblees Dec 16, 2025
9ce11d9
strbuf_readlink(): support link targets that exceed PATH_MAX
kblees Dec 16, 2025
6f6fe02
trim_last_path_component(): avoid hard-coding the directory separator
kblees Dec 16, 2025
dae450d
mingw: don't call `GetFileAttributes()` twice in `mingw_lstat()`
kblees May 12, 2015
c36848e
mingw: implement `stat()` with symlink support
kblees May 15, 2015
aa0ca80
mingw: drop the separate `do_lstat()` function
kblees May 11, 2015
8860443
mingw: let `mingw_lstat()` error early upon problems with reparse points
kblees May 23, 2015
db1d156
mingw: teach dirent about symlinks
kblees Jan 10, 2017
4c49a3d
mingw: compute the correct size for symlinks in `mingw_lstat()`
billziss-gh May 28, 2020
ad74d54
mingw: factor out the retry logic
kblees May 19, 2015
25313ce
mingw: change default of `core.symlinks` to false
kblees May 23, 2015
b698f4a
mingw: add symlink-specific error codes
kblees May 15, 2015
282aba4
mingw: handle symlinks to directories in `mingw_unlink()`
kblees May 23, 2015
5cb3b10
mingw: support renaming symlinks
kblees May 19, 2015
4992083
mingw: allow `mingw_chdir()` to change to symlink-resolved directories
kblees May 23, 2015
8fef822
mingw: implement `readlink()`
kblees May 23, 2015
1dd5f9d
mingw: implement basic `symlink()` functionality (file symlinks only)
kblees May 23, 2015
7b6dbc7
mingw: add support for symlinks to directories
kblees May 23, 2015
d3b89c2
mingw: try to create symlinks without elevated permissions
dscho May 30, 2017
2e73ab4
mingw: emulate `stat()` a little more faithfully
dscho Mar 2, 2020
817f488
mingw: special-case index entries for symlinks with buggy size
dscho Jun 4, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apply.c
Original file line number Diff line number Diff line change
Expand Up @@ -3818,7 +3818,7 @@ static int check_preimage(struct apply_state *state,
if (*ce && !(*ce)->ce_mode)
BUG("ce_mode == 0 for path '%s'", old_name);

if (trust_executable_bit)
if (trust_executable_bit || !S_ISREG(st->st_mode))
st_mode = ce_mode_from_stat(*ce, st->st_mode);
else if (*ce)
st_mode = (*ce)->ce_mode;
Expand Down
115 changes: 50 additions & 65 deletions compat/mingw.c
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,7 @@ int mingw_open (const char *filename, int oflags, ...)
int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL);
wchar_t wfilename[MAX_PATH];
open_fn_t open_fn;
WIN32_FILE_ATTRIBUTE_DATA fdata;

DECLARE_PROC_ADDR(ntdll.dll, NTSTATUS, NTAPI, RtlGetLastNtStatus, void);

Expand All @@ -653,6 +654,19 @@ int mingw_open (const char *filename, int oflags, ...)
else if (xutftowcs_path(wfilename, filename) < 0)
return -1;

/*
* When `symlink` exists and is a symbolic link pointing to a
* non-existing file, `_wopen(symlink, O_CREAT | O_EXCL)` would
* create that file. Not what we want: Linux would say `EEXIST`
* in that instance, which is therefore what Git expects.
*/
if (create &&
GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata) &&
(fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) {
errno = EEXIST;
return -1;
}

fd = open_fn(wfilename, oflags, mode);

/*
Expand Down Expand Up @@ -903,20 +917,22 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
return 1;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the Git mailing list, Johannes Sixt wrote (reply to this):

Am 17.12.25 um 15:08 schrieb Karsten Blees via GitGitGadget:
> From: Karsten Blees <blees@dcon.de>
> 
> With the new `mingw_stat()` implementation, `do_lstat()` is only called
> from `mingw_lstat()` (with the function parameter `follow == 0`). Remove
> the extra function and the old `mingw_stat()`-specific (`follow == 1`)
> logic.
> 
> Signed-off-by: Karsten Blees <blees@dcon.de>
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>  compat/mingw.c | 22 ++--------------------
>  1 file changed, 2 insertions(+), 20 deletions(-)
> 
> diff --git a/compat/mingw.c b/compat/mingw.c
> index 59afd69686..ec6c2801d3 100644
> --- a/compat/mingw.c
> +++ b/compat/mingw.c
> @@ -917,14 +917,7 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
>  	return 1;
>  }
>  
> -/* We keep the do_lstat code in a separate function to avoid recursion.
> - * When a path ends with a slash, the stat will fail with ENOENT. In
> - * this case, we strip the trailing slashes and stat again.
> - *
> - * If follow is true then act like stat() and report on the link
> - * target. Otherwise report on the link itself.
> - */
> -static int do_lstat(int follow, const char *file_name, struct stat *buf)
> +int mingw_lstat(const char *file_name, struct stat *buf)

Oh, here goes the entire function including the comment. Fine, then.
Disregard my comment on 01/18.

>  {
>  	WIN32_FILE_ATTRIBUTE_DATA fdata;
>  	wchar_t wfilename[MAX_PATH];
> @@ -958,13 +951,7 @@ static int do_lstat(int follow, const char *file_name, struct stat *buf)
>  			if (handle != INVALID_HANDLE_VALUE) {
>  				if ((findbuf.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) &&
>  						(findbuf.dwReserved0 == IO_REPARSE_TAG_SYMLINK)) {
> -					if (follow) {
> -						char buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
> -						buf->st_size = readlink(file_name, buffer, MAXIMUM_REPARSE_DATA_BUFFER_SIZE);
> -					} else {
> -						buf->st_mode = S_IFLNK;
> -					}
> -					buf->st_mode |= S_IREAD;
> +					buf->st_mode = S_IFLNK | S_IREAD;
>  					if (!(findbuf.dwFileAttributes & FILE_ATTRIBUTE_READONLY))
>  						buf->st_mode |= S_IWRITE;
>  				}
> @@ -1022,11 +1009,6 @@ static int get_file_info_by_handle(HANDLE hnd, struct stat *buf)
>  	return 0;
>  }
>  
> -int mingw_lstat(const char *file_name, struct stat *buf)
> -{
> -	return do_lstat(0, file_name, buf);
> -}
> -
>  int mingw_stat(const char *file_name, struct stat *buf)
>  {
>  	wchar_t wfile_name[MAX_PATH];

An obviously correct rewrite.

-- Hannes

}

/* We keep the do_lstat code in a separate function to avoid recursion.
* When a path ends with a slash, the stat will fail with ENOENT. In
* this case, we strip the trailing slashes and stat again.
*
* If follow is true then act like stat() and report on the link
* target. Otherwise report on the link itself.
*/
static int do_lstat(int follow, const char *file_name, struct stat *buf)
int mingw_lstat(const char *file_name, struct stat *buf)
{
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the Git mailing list, Johannes Sixt wrote (reply to this):

Am 17.12.25 um 15:08 schrieb Karsten Blees via GitGitGadget:
> From: Karsten Blees <blees@dcon.de>
> 
> The Win32 API function `GetFileAttributes()` cannot handle paths with
> trailing dir separators. The current `mingw_stat()`/`mingw_lstat()`
> implementation calls `GetFileAttributes()` twice if the path has
> trailing slashes (first with the original path that was passed as
> function parameter, and and a second time with a path copy with trailing
> '/' removed).
A comment above do_lstat() mentions this procedure. This patch doesn't
change the comment, but it should.

-- Hannes

WIN32_FILE_ATTRIBUTE_DATA fdata;
wchar_t wfilename[MAX_PATH];
if (xutftowcs_path(wfilename, file_name) < 0)
int wlen = xutftowcs_path(wfilename, file_name);
if (wlen < 0)
return -1;

/* strip trailing '/', or GetFileAttributes will fail */
while (wlen && is_dir_sep(wfilename[wlen - 1]))
wfilename[--wlen] = 0;
if (!wlen) {
errno = ENOENT;
return -1;
}

if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
buf->st_ino = 0;
buf->st_gid = 0;
Expand All @@ -935,13 +951,7 @@ static int do_lstat(int follow, const char *file_name, struct stat *buf)
if (handle != INVALID_HANDLE_VALUE) {
if ((findbuf.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) &&
(findbuf.dwReserved0 == IO_REPARSE_TAG_SYMLINK)) {
if (follow) {
char buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
buf->st_size = readlink(file_name, buffer, MAXIMUM_REPARSE_DATA_BUFFER_SIZE);
} else {
buf->st_mode = S_IFLNK;
}
buf->st_mode |= S_IREAD;
buf->st_mode = S_IFLNK | S_IREAD;
if (!(findbuf.dwFileAttributes & FILE_ATTRIBUTE_READONLY))
buf->st_mode |= S_IWRITE;
}
Expand Down Expand Up @@ -976,39 +986,6 @@ static int do_lstat(int follow, const char *file_name, struct stat *buf)
return -1;
}

/* We provide our own lstat/fstat functions, since the provided
* lstat/fstat functions are so slow. These stat functions are
* tailored for Git's usage (read: fast), and are not meant to be
* complete. Note that Git stat()s are redirected to mingw_lstat()
* too, since Windows doesn't really handle symlinks that well.
*/
static int do_stat_internal(int follow, const char *file_name, struct stat *buf)
{
size_t namelen;
char alt_name[PATH_MAX];

if (!do_lstat(follow, file_name, buf))
return 0;

/* if file_name ended in a '/', Windows returned ENOENT;
* try again without trailing slashes
*/
if (errno != ENOENT)
return -1;

namelen = strlen(file_name);
if (namelen && file_name[namelen-1] != '/')
return -1;
while (namelen && file_name[namelen-1] == '/')
--namelen;
if (!namelen || namelen >= PATH_MAX)
return -1;

memcpy(alt_name, file_name, namelen);
alt_name[namelen] = 0;
return do_lstat(follow, alt_name, buf);
}

static int get_file_info_by_handle(HANDLE hnd, struct stat *buf)
{
BY_HANDLE_FILE_INFORMATION fdata;
Expand All @@ -1032,13 +1009,25 @@ static int get_file_info_by_handle(HANDLE hnd, struct stat *buf)
return 0;
}

int mingw_lstat(const char *file_name, struct stat *buf)
{
return do_stat_internal(0, file_name, buf);
}
int mingw_stat(const char *file_name, struct stat *buf)
{
return do_stat_internal(1, file_name, buf);
wchar_t wfile_name[MAX_PATH];
HANDLE hnd;
int result;

/* open the file and let Windows resolve the links */
if (xutftowcs_path(wfile_name, file_name) < 0)
return -1;
hnd = CreateFileW(wfile_name, 0,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (hnd == INVALID_HANDLE_VALUE) {
errno = err_win_to_posix(GetLastError());
return -1;
}
result = get_file_info_by_handle(hnd, buf);
CloseHandle(hnd);
return result;
}

int mingw_fstat(int fd, struct stat *buf)
Expand Down Expand Up @@ -1225,18 +1214,16 @@ char *mingw_getcwd(char *pointer, int len)
{
wchar_t cwd[MAX_PATH], wpointer[MAX_PATH];
DWORD ret = GetCurrentDirectoryW(ARRAY_SIZE(cwd), cwd);
HANDLE hnd;

if (!ret || ret >= ARRAY_SIZE(cwd)) {
errno = ret ? ENAMETOOLONG : err_win_to_posix(GetLastError());
return NULL;
}
ret = GetLongPathNameW(cwd, wpointer, ARRAY_SIZE(wpointer));
if (!ret && GetLastError() == ERROR_ACCESS_DENIED) {
HANDLE hnd = CreateFileW(cwd, 0,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (hnd == INVALID_HANDLE_VALUE)
return NULL;
hnd = CreateFileW(cwd, 0,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (hnd != INVALID_HANDLE_VALUE) {
ret = GetFinalPathNameByHandleW(hnd, wpointer, ARRAY_SIZE(wpointer), 0);
CloseHandle(hnd);
if (!ret || ret >= ARRAY_SIZE(wpointer))
Expand All @@ -1245,13 +1232,11 @@ char *mingw_getcwd(char *pointer, int len)
return NULL;
return pointer;
}
if (!ret || ret >= ARRAY_SIZE(wpointer))
return NULL;
if (GetFileAttributesW(wpointer) == INVALID_FILE_ATTRIBUTES) {
if (GetFileAttributesW(cwd) == INVALID_FILE_ATTRIBUTES) {
errno = ENOENT;
return NULL;
}
if (xwcstoutf(pointer, wpointer, len) < 0)
if (xwcstoutf(pointer, cwd, len) < 0)
return NULL;
convert_slashes(pointer);
return pointer;
Expand Down
4 changes: 2 additions & 2 deletions environment.c
Original file line number Diff line number Diff line change
Expand Up @@ -324,8 +324,8 @@ static enum fsync_component parse_fsync_components(const char *var, const char *
return (current & ~negative) | positive;
}

static int git_default_core_config(const char *var, const char *value,
const struct config_context *ctx, void *cb)
int git_default_core_config(const char *var, const char *value,
const struct config_context *ctx, void *cb)
{
/* This needs a better name */
if (!strcmp(var, "core.filemode")) {
Expand Down
2 changes: 2 additions & 0 deletions environment.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ const char *strip_namespace(const char *namespaced_ref);

int git_default_config(const char *, const char *,
const struct config_context *, void *);
int git_default_core_config(const char *var, const char *value,
const struct config_context *ctx, void *cb);

/*
* TODO: All the below state either explicitly or implicitly relies on
Expand Down
4 changes: 2 additions & 2 deletions lockfile.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ static void trim_last_path_component(struct strbuf *path)
int i = path->len;

/* back up past trailing slashes, if any */
while (i && path->buf[i - 1] == '/')
while (i && is_dir_sep(path->buf[i - 1]))
i--;

/*
* then go backwards until a slash, or the beginning of the
* string
*/
while (i && path->buf[i - 1] != '/')
while (i && !is_dir_sep(path->buf[i - 1]))
i--;

strbuf_setlen(path, i);
Expand Down
2 changes: 1 addition & 1 deletion setup.c
Original file line number Diff line number Diff line change
Expand Up @@ -2693,7 +2693,7 @@ int init_db(const char *git_dir, const char *real_git_dir,
* have set up the repository format such that we can evaluate
* includeIf conditions correctly in the case of re-initialization.
*/
repo_config(the_repository, platform_core_config, NULL);
repo_config(the_repository, git_default_core_config, NULL);

safe_create_dir(the_repository, git_dir, 0);

Expand Down
10 changes: 4 additions & 6 deletions strbuf.c
Original file line number Diff line number Diff line change
Expand Up @@ -566,24 +566,22 @@ ssize_t strbuf_write(struct strbuf *sb, FILE *f)
return sb->len ? fwrite(sb->buf, 1, sb->len, f) : 0;
}

#define STRBUF_MAXLINK (2*PATH_MAX)

int strbuf_readlink(struct strbuf *sb, const char *path, size_t hint)
{
size_t oldalloc = sb->alloc;

if (hint < 32)
hint = 32;

while (hint < STRBUF_MAXLINK) {
for (;;) {
ssize_t len;

strbuf_grow(sb, hint);
len = readlink(path, sb->buf, hint);
strbuf_grow(sb, hint + 1);
len = readlink(path, sb->buf, hint + 1);
if (len < 0) {
if (errno != ERANGE)
break;
} else if (len < hint) {
} else if (len <= hint) {
strbuf_setlen(sb, len);
return 0;
}
Expand Down
6 changes: 5 additions & 1 deletion t/t0001-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,11 @@ test_expect_success SYMLINKS 're-init to move gitdir symlink' '
git init --separate-git-dir ../realgitdir
) &&
echo "gitdir: $(pwd)/realgitdir" >expected &&
test_cmp expected newdir/.git &&
case "$GIT_TEST_CMP" in
# `git diff --no-index` does not resolve symlinks
*--no-index*) cmp expected newdir/.git;;
*) test_cmp expected newdir/.git;;
esac &&
test_cmp expected newdir/here &&
test_path_is_dir realgitdir/refs
'
Expand Down
3 changes: 2 additions & 1 deletion t/t0301-credential-cache.sh
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ test_expect_success SYMLINKS 'use user socket if user directory is a symlink to
rmdir \"\$HOME/dir/\" &&
rm \"\$HOME/.git-credential-cache\"
" &&
mkdir -p -m 700 "$HOME/dir/" &&
mkdir -p "$HOME/dir/" &&
chmod 700 "$HOME/dir/" &&
ln -s "$HOME/dir" "$HOME/.git-credential-cache" &&
check approve cache <<-\EOF &&
protocol=https
Expand Down
2 changes: 1 addition & 1 deletion t/t0600-reffiles-backend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ test_expect_success POSIXPERM 'git reflog expire honors core.sharedRepository' '
esac
'

test_expect_success SYMLINKS 'symref transaction supports symlinks' '
test_expect_success SYMLINKS,!MINGW 'symref transaction supports symlinks' '
test_when_finished "git symbolic-ref -d TEST_SYMREF_HEAD" &&
git update-ref refs/heads/new @ &&
test_config core.prefersymlinkrefs true &&
Expand Down
24 changes: 17 additions & 7 deletions t/t1006-cat-file.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1048,18 +1048,28 @@ test_expect_success 'git cat-file --batch-check --follow-symlinks works for out-
echo .. >>expect &&
echo HEAD:dir/subdir/out-of-repo-link-dir | git cat-file --batch-check --follow-symlinks >actual &&
test_cmp expect actual &&
echo symlink 3 >expect &&
echo ../ >>expect &&
if test_have_prereq MINGW,SYMLINKS
then
test_write_lines "symlink 2" ..
else
test_write_lines "symlink 3" ../
fi >expect &&
echo HEAD:dir/subdir/out-of-repo-link-dir-trailing | git cat-file --batch-check --follow-symlinks >actual &&
test_cmp expect actual
'

test_expect_success 'git cat-file --batch-check --follow-symlinks works for symlinks with internal ..' '
echo HEAD: | git cat-file --batch-check >expect &&
echo HEAD:up-down | git cat-file --batch-check --follow-symlinks >actual &&
test_cmp expect actual &&
echo HEAD:up-down-trailing | git cat-file --batch-check --follow-symlinks >actual &&
test_cmp expect actual &&
if test_have_prereq !MINGW
then
# The `up-down` and `up-down-trailing` symlinks are normalized
# in MSYS in `winsymlinks` mode and are therefore in a
# different shape than Git expects them.
echo HEAD: | git cat-file --batch-check >expect &&
echo HEAD:up-down | git cat-file --batch-check --follow-symlinks >actual &&
test_cmp expect actual &&
echo HEAD:up-down-trailing | git cat-file --batch-check --follow-symlinks >actual &&
test_cmp expect actual
fi &&
echo HEAD:up-down-file | git cat-file --batch-check --follow-symlinks >actual &&
test_cmp found actual &&
echo symlink 7 >expect &&
Expand Down
4 changes: 2 additions & 2 deletions t/t1305-config-include.sh
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ test_expect_success SYMLINKS 'conditional include, relative path with symlinks'
)
'

test_expect_success SYMLINKS 'conditional include, gitdir matching symlink' '
test_expect_success SYMLINKS,!MINGW 'conditional include, gitdir matching symlink' '
ln -s foo bar &&
(
cd bar &&
Expand All @@ -298,7 +298,7 @@ test_expect_success SYMLINKS 'conditional include, gitdir matching symlink' '
)
'

test_expect_success SYMLINKS 'conditional include, gitdir matching symlink, icase' '
test_expect_success SYMLINKS,!MINGW 'conditional include, gitdir matching symlink, icase' '
(
cd bar &&
echo "[includeIf \"gitdir/i:BAR/\"]path=bar8" >>.git/config &&
Expand Down
9 changes: 7 additions & 2 deletions t/t6423-merge-rename-directories.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5158,13 +5158,18 @@ test_setup_12m () {
git switch B &&
git rm dir/subdir/file &&
mkdir dir &&
ln -s /dev/null dir/subdir &&
if test_have_prereq MINGW
then
cmd //c 'mklink dir\subdir NUL'
else
ln -s /dev/null dir/subdir
fi &&
git add . &&
git commit -m "B"
)
}

test_expect_success '12m: Change parent of renamed-dir to symlink on other side' '
test_expect_success SYMLINKS '12m: Change parent of renamed-dir to symlink on other side' '
test_setup_12m &&
(
cd 12m &&
Expand Down
Loading