Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 39 additions & 0 deletions path.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,45 @@
#include "lockfile.h"
#include "exec-cmd.h"

void translate_wsl_path(char *path)
{
#ifdef GIT_WINDOWS_NATIVE
static const struct {
const char *prefix;
size_t prefix_len;
} posix_prefixes[] = {
{ "/mnt/", 5 },
{ "/cygdrive/", 10 },
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That works for Cygwin. But Git for Windows is based on MSYS2, where the drives are mapped as /c/, /d/, etc. And by translating those always, you run now create ambiguities that cannot be overcome: In WSL, /c/ is a valid path that you could now no longer access. Sure, you can argue that "nobody maps those", but here is a person speaking to you who did that in the past, and in general it is not a good sign when your code introduces ambiguities where there were none before the patch.

Copy link
Copy Markdown
Author

@johnnyshields johnnyshields May 7, 2026

Choose a reason for hiding this comment

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

Firstly, WSL /mnt/c/ and Cygwin /cygwin/c/ paths are unambiguous and are major wins--especially WSL--even if we can't agree what to do with MSYS2.

Re: MSYS2, I can think of 4 possibilities:

  1. Alias /c/, /d/ to C:\, D:\ etc--I think this is arguably safe, see below.
  2. Do nothing (or defer implementation pending further discussion)
  3. Opt-in via config--something like git config --global core.translateMsysPaths true
  4. Solve it write-side--have MSYS2 write gitdir = /c/... as C:\... and then read it back as /c/. This is defensible IMHO because MSYS2 is highly specialized to Windows, and storing paths in Windows format is not wrong, per se.

Why I think MSYS2's /c/, /d/, paths etc. are safe to alias too

Recall that two gates limit where the new logic runs:

  1. Compile-time: the helper is #ifdef GIT_WINDOWS_NATIVE, so it's compiled in only on the mingw64 native build of Git for Windows. WSL git, Cygwin git, and the MSYS2-runtime git from pacman are separate builds and unaffected. Your "person who mkdir /c in WSL" continues to use a WSL git binary that has none of this code.
  2. Call site: the helper fires only on three specific worktree-related files — /.git, /worktrees//gitdir, and /commondir. It is not part of general path resolution.

For an ambiguity to actually bite a user, ALL of the following must coincide:

  1. The user runs Git-for-Windows's mingw64 git from within Windows OS
  2. They are operating on a worktree (not a normal repo).
  3. The user will have had to created a specific single-letter path /c/ within WSL Linux's root dir.
  4. The main repo must lives within this special WSL single-letter path (e.g. /c/foo, not visible to native Windows).
  5. The worktree lives at a separate Windows-visible path (e.g. /mnt/c/elsewhere/wt), i.e. not co-located with its parent repo, which would be exceedingly uncommon.
  6. The user also must have a real Windows repo at C:/foo whose exact path mirrors/clashes with the WSL one /c/foo (otherwise its a no-op)

Even with all six aligned, the failure mode is bounded: Git-for-Windows follows the translated c:/foo, finds either nothing (and does a no-op, as it does today) or a real repo at that path that the user explicitly created. AFAIK Git has protection mechanisms that prevent worktrees from pointing to the wrong copy of their repo (based on bi-directional registration.)

TLDR; I just don't see this as something that's seriously going to affect anyone, meanwhile the convenience of Windows-and-MSYS2 interop is a tangible quality-of-life win.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The path /mnt/ on Linux can be anything, from a mount to a regular directory. Nothing, nothing at all guarantees that /mnt/c refers to the mounted C: drive iff this is a WSL instance. But as I had pointed out already (which you ignored), the same code runs also on regular Linux, where it definitely does not refer to a host's C: drive because there is none.

I understand that it is in fashion nowadays to double-down instead of fixing designs, but this is not going to work here. There are fundamental problems with the proposed design that won't stand a chance of being addressed merely by doubling down on a flawed idea.

Copy link
Copy Markdown
Author

@johnnyshields johnnyshields May 7, 2026

Choose a reason for hiding this comment

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

Hold on.

Yes, /mnt/ can technically be anything. However, 99% of Linux distros use /mnt/ as part of the Linux FHS--even FHS-deviants like NixOS use /mnt/--and doubly-so for the subset of distros that are used on WSL.

Again, this PR is for Git for Windows exclusively. It is simply saying, "If Git runs on Windows, and it sees a worktree with gitdir = /mnt/c/foo/bar, it is reasonable to try to map it to C:\foo\bar", because in 99.99% of cases this arises from creating the worktree inside WSL, exactly as was reported in #6187.

So we're talking cross-purposes here. Of course we can sit around all day and say "what ifs" i.e. "what if someone renames /mnt/ in Linux?" And in fact, those cases are fine, the behavior simply degrades to the no-op that is is today. So users who want Windows-WSL git interop need to live with not renaming /mnt/ to something screwy (which by the way is going to break a whole lot more than git :)

Perhaps a compromise here is to have feature flags:

  • git config --global core.enableWindowsWSLCompatibility true
  • git config --global core.enableWindowsCygwinCompatibility true
  • git config --global core.enableWindowsMSYSCompatibility true

These would enable compatibility shims (gitdir for now, maybe other things in the future) that come with certain assumptions, i.e. that you haven't renamed /mnt/. For WSL specifically we can add the caveat about performance to the documentation.

Would you be willing to accept a PR with those feature flags added?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You are elevating a fragile heuristic to a fundamental design decision. I find that highly unconvincing, and am unwilling to accept it into Git for Windows. Maybe you will have more luck getting it into Git for Windows via the Git project.

Copy link
Copy Markdown
Author

@johnnyshields johnnyshields May 7, 2026

Choose a reason for hiding this comment

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

@dscho that /mnt/ is used by 100% of WSL-available Git distros as the mountpoint dir is not a random "fragile heuristic", its a direct consequence of the FHS standard. Anyone who renames it must reasonably expect that various things will break.

We are getting lost in the weeds here.

This PR is simply proposing to have Git on Windows interpret gitdir = /mnt/c/<path> as gitdir = C:\

  • This will benefit the large number of Windows devs using Windows and WSL with a shared filesystem.
  • If the user is not using WSL, there is no impact, because their gitdir paths will not include /mnt/
  • If the user insists to rename /mnt/ then this feature will gracefully degrade to a no-op--the exact same as it is today.
  • MSYS2 and Cygwin can be omitted for now; WSL is the priority as per Worktrees don't work when the worktree is created on Windows but used from WSL #6187.
  • I'd be happy to feature flag this in some form.

I think we need to honestly ask who is Git for Windows intended for:

  • Is it the hypothetical Windows Mega-Wizard who never uses WSL, and when he does use WSL he insists on renaming /mnt/?
  • Or the average Windows dev who installs generic WSL Ubuntu, TortoiseGit, and is just happy if his worktrees are accessible from both under the "out-of-the-box" config--i.e. "it just works" under the standard 99% happy path.

};
size_t i, len;

if (!path || !path[0])
return;
len = strlen(path);

for (i = 0; i < ARRAY_SIZE(posix_prefixes); i++) {
size_t pl = posix_prefixes[i].prefix_len;
char drive;

if (len < pl + 1)
continue;
if (memcmp(path, posix_prefixes[i].prefix, pl) != 0)
continue;
drive = path[pl];
if (!isalpha((unsigned char)drive))
continue;
if (len > pl + 1 && path[pl + 1] != '/' && path[pl + 1] != '\\')
continue;

/* In-place rewrite to "<drive>:" + tail. Result is shorter. */
path[0] = drive;
path[1] = ':';
memmove(path + 2, path + pl + 1, len - pl);
return;
}
#endif
}

static int get_st_mode_bits(const char *path, int *mode)
{
struct stat st;
Expand Down
9 changes: 9 additions & 0 deletions path.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ struct strbuf;
struct string_list;
struct worktree;

/*
* Translate POSIX-style drive paths produced by Git running under WSL2
* (`/mnt/<x>/...`) or Cygwin/MSYS (`/cygdrive/<x>/...`) into Windows
* drive-letter form (`<x>:/...`). Edits `path` in place; the result is
* never longer than the input. No-op on non-Windows platforms, where
* these prefixes may name real directories on the host filesystem.
*/
void translate_wsl_path(char *path);

/*
* The result to all functions which return statically allocated memory may be
* overwritten by another call to _any_ one of these functions. Consider using
Expand Down
3 changes: 3 additions & 0 deletions setup.c
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ int get_common_dir_noenv(struct strbuf *sb, const char *gitdir)
data.len--;
data.buf[data.len] = '\0';
strbuf_reset(&path);
translate_wsl_path(data.buf);
if (!is_absolute_path(data.buf))
strbuf_addf(&path, "%s/", gitdir);
strbuf_addbuf(&path, &data);
Expand Down Expand Up @@ -1009,6 +1010,8 @@ const char *read_gitfile_gently(const char *path, int *return_error_code)
buf[len] = '\0';
dir = buf + 8;

translate_wsl_path(dir);

if (!is_absolute_path(dir) && (slash = strrchr(path, '/'))) {
size_t pathlen = slash+1 - path;
dir = xstrfmt("%.*s%.*s", (int)pathlen, path,
Expand Down
135 changes: 135 additions & 0 deletions t/t0042-wsl-mnt-path.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/bin/sh

test_description='translate WSL/Cygwin /mnt/<x>/ paths in worktree gitfiles

Verify that `git worktree add` artefacts written from inside WSL2 or
Cygwin/MSYS - which use POSIX-mounted paths like `/mnt/c/...` or
`/cygdrive/c/...` - are still resolvable when read back from native
Windows git.
'

GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME

. ./test-lib.sh

# Convert any drive-prefixed path Windows git might emit to the named
# POSIX-mount form. Handles MSYS form (/c/foo) and Windows forms
# (C:/foo and C:\foo). MINGW-only.
mount_form () {
prefix=$1 ;# /mnt or /cygdrive
path=$2
case "$path" in
/[A-Za-z]/*)
echo "$path" | sed -E "s|^/([A-Za-z])/|$prefix/\\L\\1/|"
;;
[A-Za-z]:/*)
echo "$path" | sed -E "s|^([A-Za-z]):/|$prefix/\\L\\1/|"
;;
[A-Za-z]:'\'*)
echo "$path" | sed -E "s|^([A-Za-z]):.|$prefix/\\L\\1/|; s|\\\\|/|g"
;;
*)
echo "$path"
;;
esac
}

to_mnt () {
mount_form /mnt "$1"
}

to_cygdrive () {
mount_form /cygdrive "$1"
}

test_expect_success MINGW 'setup main repo' '
git init repo &&
test_commit -C repo init
'

test_expect_success MINGW 'read_gitfile_gently translates /mnt/<x>/ gitdir' '
test_when_finished "rm -rf wtlink actual" &&
REAL=$(cd repo/.git && pwd) &&
MNT=$(to_mnt "$REAL") &&

# Sanity: the path must actually start with /mnt/ - if it does not,
# the host shell did not give us a path with a drive prefix and the
# rest of the test would be silently meaningless.
case "$MNT" in
/mnt/*) : ok ;;
*) BUG "to_mnt produced $MNT from $REAL" ;;
esac &&

mkdir wtlink &&
printf "gitdir: %s\n" "$MNT" >wtlink/.git &&

(cd wtlink && git rev-parse --git-dir) >actual &&
test_path_is_dir "$(cat actual)"
'

test_expect_success MINGW 'read_gitfile_gently translates /cygdrive/<x>/ gitdir' '
test_when_finished "rm -rf wtlink actual" &&
REAL=$(cd repo/.git && pwd) &&
CYG=$(to_cygdrive "$REAL") &&

mkdir wtlink &&
printf "gitdir: %s\n" "$CYG" >wtlink/.git &&

(cd wtlink && git rev-parse --git-dir) >actual &&
test_path_is_dir "$(cat actual)"
'

test_expect_success MINGW 'read_gitfile_gently leaves /mnt/<multichar>/ alone' '
test_when_finished "rm -rf wtlink" &&
mkdir wtlink &&
# "storage" is not a single drive letter, so this must not be
# translated. The path does not exist on Windows, so the open fails.
echo "gitdir: /mnt/storage/no/such/repo" >wtlink/.git &&

test_must_fail git -C wtlink rev-parse --git-dir 2>err &&
test_grep "not a git repository" err
'

test_expect_success MINGW 'get_linked_worktree finds worktree recorded with /mnt/<x>/ path' '
test_when_finished "rm -rf repo/wt repo/.git/worktrees/wt" &&

git -C repo worktree add --detach wt &&
WT_REAL=$(cd repo/wt && pwd) &&
WT_MNT=$(to_mnt "$WT_REAL") &&

# Overwrite the recorded worktree path with the WSL form, mimicking
# what `git worktree add` writes when run from inside WSL.
printf "%s/.git\n" "$WT_MNT" >repo/.git/worktrees/wt/gitdir &&

# `git worktree list` reads that file via get_linked_worktree.
# After translation the worktree must still be reachable: it must
# NOT be flagged prunable, and a git operation inside the worktree
# directory must succeed.
git -C repo worktree list --porcelain >list &&
! grep -q "^prunable" list &&
(cd "$WT_REAL" && git rev-parse --is-inside-work-tree)
'

test_expect_success MINGW 'get_common_dir_noenv translates /mnt/<x>/ commondir' '
test_when_finished "rm -rf wtdir wt actual" &&

REAL=$(cd repo/.git && pwd) &&
MNT=$(to_mnt "$REAL") &&

# Build a synthetic linked-worktree gitdir that points at the main
# repo via a /mnt/<x>/ commondir record.
mkdir wtdir &&
echo "$(cd repo && git rev-parse HEAD)" >wtdir/HEAD &&
echo "$MNT" >wtdir/commondir &&
printf "%s/.git\n" "$(pwd)" >wtdir/gitdir &&

# rev-parse --git-common-dir on a checkout that points here should
# resolve through the translated commondir.
mkdir wt &&
printf "gitdir: %s\n" "$(pwd)/wtdir" >wt/.git &&
(cd wt && git rev-parse --git-common-dir) >actual &&
test_path_is_dir "$(cat actual)"
'

test_done
5 changes: 5 additions & 0 deletions worktree.c
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ struct worktree *get_linked_worktree(const char *id,
strbuf_rtrim(&worktree_path);
strbuf_strip_suffix(&worktree_path, "/.git");

/* Worktree path may have been recorded under WSL/Cygwin. */
translate_wsl_path(worktree_path.buf);

if (!is_absolute_path(worktree_path.buf)) {
strbuf_strip_suffix(&path, "gitdir");
strbuf_addbuf(&path, &worktree_path);
Expand Down Expand Up @@ -992,6 +995,8 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath,
goto done;
}
path[len] = '\0';
/* Worktree path may have been recorded under WSL/Cygwin. */
translate_wsl_path(path);
if (is_absolute_path(path)) {
strbuf_addstr(&dotgit, path);
} else {
Expand Down