Skip to content

Commit 555e8fa

Browse files
committed
path.c: translate WSL and Cygwin paths when resolving worktrees
When `git worktree add` is run from inside WSL2 (or Cygwin/MSYS), git records each worktree's path using the POSIX-mounted view of the Windows filesystem - e.g. `/mnt/c/repo/.git/worktrees/wt` or `/cygdrive/c/repo/.git/worktrees/wt`. Reading those files back from native-Windows git fails because the paths are not meaningful outside of the WSL/Cygwin namespace, so the worktree appears broken even though every byte of it is reachable from Windows. Add a small helper `translate_wsl_path()` that rewrites the recognised POSIX-drive prefixes into Windows drive-letter form in place, and call it at the three places where Windows-native git reads a recorded worktree-related path back from disk: * `read_gitfile_gently()` - the `gitdir:` line in a worktree's `.git` file. * `get_common_dir_noenv()` - the `commondir` file inside a worktree's git directory, which points at the main repo. * `get_linked_worktree()` - the `gitdir` file inside `<commondir>/worktrees/<id>/`, which points at the worktree's `.git` link. Translation only happens for `/mnt/<x>/` and `/cygdrive/<x>/` where `<x>` is a single ASCII letter and the next character is either a separator or end-of-string. Anything else (e.g. `/mnt/storage`) is left alone. The helper compiles to a no-op outside `GIT_WINDOWS_NATIVE`, so a `/mnt/c/...` path on a Linux host - where it might really exist - is still treated literally. Add t/t0042-wsl-mnt-path.sh covering all three call sites plus the "do not translate" cases. Tests are gated on the MINGW prereq. Signed-off-by: johnnyshields <27655+johnnyshields@users.noreply.github.com>
1 parent 01883ac commit 555e8fa

5 files changed

Lines changed: 167 additions & 0 deletions

File tree

path.c

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,45 @@
1818
#include "lockfile.h"
1919
#include "exec-cmd.h"
2020

21+
void translate_wsl_path(char *path)
22+
{
23+
#ifdef GIT_WINDOWS_NATIVE
24+
static const struct {
25+
const char *prefix;
26+
size_t prefix_len;
27+
} posix_prefixes[] = {
28+
{ "/mnt/", 5 },
29+
{ "/cygdrive/", 10 },
30+
};
31+
size_t i, len;
32+
33+
if (!path || !path[0])
34+
return;
35+
len = strlen(path);
36+
37+
for (i = 0; i < ARRAY_SIZE(posix_prefixes); i++) {
38+
size_t pl = posix_prefixes[i].prefix_len;
39+
char drive;
40+
41+
if (len < pl + 1)
42+
continue;
43+
if (memcmp(path, posix_prefixes[i].prefix, pl) != 0)
44+
continue;
45+
drive = path[pl];
46+
if (!isalpha((unsigned char)drive))
47+
continue;
48+
if (len > pl + 1 && path[pl + 1] != '/' && path[pl + 1] != '\\')
49+
continue;
50+
51+
/* In-place rewrite to "<drive>:" + tail. Result is shorter. */
52+
path[0] = drive;
53+
path[1] = ':';
54+
memmove(path + 2, path + pl + 1, len - pl);
55+
return;
56+
}
57+
#endif
58+
}
59+
2160
static int get_st_mode_bits(const char *path, int *mode)
2261
{
2362
struct stat st;

path.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ struct strbuf;
66
struct string_list;
77
struct worktree;
88

9+
/*
10+
* Translate POSIX-style drive paths produced by Git running under WSL2
11+
* (`/mnt/<x>/...`) or Cygwin/MSYS (`/cygdrive/<x>/...`) into Windows
12+
* drive-letter form (`<x>:/...`). Edits `path` in place; the result is
13+
* never longer than the input. No-op on non-Windows platforms, where
14+
* these prefixes may name real directories on the host filesystem.
15+
*/
16+
void translate_wsl_path(char *path);
17+
918
/*
1019
* The result to all functions which return statically allocated memory may be
1120
* overwritten by another call to _any_ one of these functions. Consider using

setup.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ int get_common_dir_noenv(struct strbuf *sb, const char *gitdir)
336336
data.len--;
337337
data.buf[data.len] = '\0';
338338
strbuf_reset(&path);
339+
translate_wsl_path(data.buf);
339340
if (!is_absolute_path(data.buf))
340341
strbuf_addf(&path, "%s/", gitdir);
341342
strbuf_addbuf(&path, &data);
@@ -1009,6 +1010,8 @@ const char *read_gitfile_gently(const char *path, int *return_error_code)
10091010
buf[len] = '\0';
10101011
dir = buf + 8;
10111012

1013+
translate_wsl_path(dir);
1014+
10121015
if (!is_absolute_path(dir) && (slash = strrchr(path, '/'))) {
10131016
size_t pathlen = slash+1 - path;
10141017
dir = xstrfmt("%.*s%.*s", (int)pathlen, path,

t/t0042-wsl-mnt-path.sh

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/bin/sh
2+
3+
test_description='translate WSL/Cygwin /mnt/<x>/ paths in worktree gitfiles
4+
5+
Verify that `git worktree add` artefacts written from inside WSL2 or
6+
Cygwin/MSYS - which use POSIX-mounted paths like `/mnt/c/...` or
7+
`/cygdrive/c/...` - are still resolvable when read back from native
8+
Windows git.
9+
'
10+
11+
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
12+
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
13+
14+
. ./test-lib.sh
15+
16+
# Convert an MSYS path (/c/foo) to a WSL path (/mnt/c/foo).
17+
# MINGW-only; on other platforms /c/foo is not a drive prefix.
18+
to_mnt () {
19+
echo "$1" | sed -E 's|^/([a-zA-Z])/|/mnt/\L\1/|'
20+
}
21+
22+
# Same, for Cygwin form (/cygdrive/c/foo).
23+
to_cygdrive () {
24+
echo "$1" | sed -E 's|^/([a-zA-Z])/|/cygdrive/\L\1/|'
25+
}
26+
27+
test_expect_success MINGW 'setup main repo' '
28+
git init repo &&
29+
test_commit -C repo init
30+
'
31+
32+
test_expect_success MINGW 'read_gitfile_gently translates /mnt/<x>/ gitdir' '
33+
test_when_finished "rm -rf wtlink actual" &&
34+
REAL=$(cd repo/.git && pwd) &&
35+
MNT=$(to_mnt "$REAL") &&
36+
37+
# Sanity: the path must actually start with /mnt/ - if it does not,
38+
# the host shell did not give us a path with a drive prefix and the
39+
# rest of the test would be silently meaningless.
40+
case "$MNT" in
41+
/mnt/*) : ok ;;
42+
*) BUG "to_mnt produced $MNT from $REAL" ;;
43+
esac &&
44+
45+
mkdir wtlink &&
46+
printf "gitdir: %s\n" "$MNT" >wtlink/.git &&
47+
48+
(cd wtlink && git rev-parse --git-dir) >actual &&
49+
test_path_is_dir "$(cat actual)"
50+
'
51+
52+
test_expect_success MINGW 'read_gitfile_gently translates /cygdrive/<x>/ gitdir' '
53+
test_when_finished "rm -rf wtlink actual" &&
54+
REAL=$(cd repo/.git && pwd) &&
55+
CYG=$(to_cygdrive "$REAL") &&
56+
57+
mkdir wtlink &&
58+
printf "gitdir: %s\n" "$CYG" >wtlink/.git &&
59+
60+
(cd wtlink && git rev-parse --git-dir) >actual &&
61+
test_path_is_dir "$(cat actual)"
62+
'
63+
64+
test_expect_success MINGW 'read_gitfile_gently leaves /mnt/<multichar>/ alone' '
65+
test_when_finished "rm -rf wtlink" &&
66+
mkdir wtlink &&
67+
# "storage" is not a single drive letter, so this must not be
68+
# translated. The path does not exist on Windows, so the open fails.
69+
echo "gitdir: /mnt/storage/no/such/repo" >wtlink/.git &&
70+
71+
test_must_fail git -C wtlink rev-parse --git-dir 2>err &&
72+
test_grep "not a git repository" err
73+
'
74+
75+
test_expect_success MINGW 'get_linked_worktree finds worktree recorded with /mnt/<x>/ path' '
76+
test_when_finished "rm -rf repo/wt repo/.git/worktrees/wt" &&
77+
78+
git -C repo worktree add --detach wt &&
79+
WT_REAL=$(cd repo/wt && pwd) &&
80+
WT_MNT=$(to_mnt "$WT_REAL") &&
81+
82+
# Overwrite the recorded worktree path with the WSL form, mimicking
83+
# what `git worktree add` writes when run from inside WSL.
84+
printf "%s/.git\n" "$WT_MNT" >repo/.git/worktrees/wt/gitdir &&
85+
86+
# `git worktree list` reads that file via get_linked_worktree.
87+
# After translation the worktree should still be enumerated.
88+
git -C repo worktree list --porcelain >list &&
89+
grep -F "worktree $WT_REAL" list
90+
'
91+
92+
test_expect_success MINGW 'get_common_dir_noenv translates /mnt/<x>/ commondir' '
93+
test_when_finished "rm -rf wtdir wt actual" &&
94+
95+
REAL=$(cd repo/.git && pwd) &&
96+
MNT=$(to_mnt "$REAL") &&
97+
98+
# Build a synthetic linked-worktree gitdir that points at the main
99+
# repo via a /mnt/<x>/ commondir record.
100+
mkdir wtdir &&
101+
echo "$(cd repo && git rev-parse HEAD)" >wtdir/HEAD &&
102+
echo "$MNT" >wtdir/commondir &&
103+
printf "%s/.git\n" "$(pwd)" >wtdir/gitdir &&
104+
105+
# rev-parse --git-common-dir on a checkout that points here should
106+
# resolve through the translated commondir.
107+
mkdir wt &&
108+
printf "gitdir: %s\n" "$(pwd)/wtdir" >wt/.git &&
109+
(cd wt && git rev-parse --git-common-dir) >actual &&
110+
test_path_is_dir "$(cat actual)"
111+
'
112+
113+
test_done

worktree.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ struct worktree *get_linked_worktree(const char *id,
155155
strbuf_rtrim(&worktree_path);
156156
strbuf_strip_suffix(&worktree_path, "/.git");
157157

158+
/* Worktree path may have been recorded under WSL/Cygwin. */
159+
translate_wsl_path(worktree_path.buf);
160+
158161
if (!is_absolute_path(worktree_path.buf)) {
159162
strbuf_strip_suffix(&path, "gitdir");
160163
strbuf_addbuf(&path, &worktree_path);

0 commit comments

Comments
 (0)