Skip to content

Commit 9ea2fc1

Browse files
committed
path.c: translate worktree paths between Windows and WSL/Cygwin builds
A `git worktree` created by git running under one runtime is in general not openable by git running under another, because the paths recorded in the worktree's metadata files (`<worktree>/.git`, `<commondir>/worktrees/<id>/gitdir`, `<commondir>/commondir`) are written in the originating runtime's form. The two common cases: 1. Worktree created from WSL2 (or Cygwin/MSYS), opened by native Windows git. Recorded paths look like `/mnt/c/...` or `/cygdrive/c/...` — not parseable by Win32 APIs. 2. Worktree created from native Windows, opened by WSL2 / Cygwin / MSYS2 git. Recorded paths look like `C:/...` or `C:\...` — not valid POSIX paths. In either case the worktree appears broken even though every byte of it is reachable from the reader. Add a single helper `translate_windows_path()` that rewrites recorded paths to the form expected by the current build. Direction is selected at compile time, not at runtime: * On `GIT_WINDOWS_NATIVE` builds, `/mnt/<x>/...` or `/cygdrive/<x>/...` (where `<x>` is a single ASCII letter followed by `/`, `\`, or end-of-string) are rewritten in place to `<x>:/...`. * On other builds, `<x>:/...` and `<x>:\...` are rewritten to the mount form for this runtime: `/<x>/...` on MSYS2, `/cygdrive/<x>/...` on real Cygwin, `/mnt/<x>/...` everywhere else (the WSL2 default; harmless on hosts where `/mnt/<x>/` is not a Windows-drive mount, because the translated path simply fails to resolve, no worse than the unparseable input). Backslashes in the remainder are normalised to forward slashes. Multi-character segments (`/mnt/storage`, `/cygdrive/usr`) and digit-prefixed mounts pass through untouched, so legitimate POSIX paths under these prefixes are never disturbed. Wire the helper into the four sites that read recorded worktree path metadata: * `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. * `get_linked_worktree()` — the `gitdir` file inside `<commondir>/worktrees/<id>/`. * `should_prune_worktree()` — re-reads the same file when deciding prunability; without translation, a cross-runtime worktree would be marked `prunable gitdir file points to non-existent location` even when the listing succeeded. Add tests: * `t/t0060-path-utils.sh` exercises the helper directly via a new `translate_windows_path` subcommand of `test-tool path-utils`, covering both translatable shapes and shapes that must remain untouched. The expected mount root is selected from `uname -s`, so the suite passes on Cygwin, MSYS2, and Linux/WSL builds. * `t/t0042-wsl-mnt-path.sh` (MINGW-gated) exercises all four read sites end-to-end using a real worktree whose recorded paths have been rewritten in `/mnt/<x>/` form, mimicking git running inside WSL2.
1 parent 947de87 commit 9ea2fc1

6 files changed

Lines changed: 269 additions & 30 deletions

File tree

path.c

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,74 @@
2020

2121
int translate_windows_path(struct strbuf *path)
2222
{
23-
#ifndef GIT_WINDOWS_NATIVE
24-
#ifdef __CYGWIN__
23+
#ifdef GIT_WINDOWS_NATIVE
24+
/*
25+
* Native-Windows direction: paths recorded by git running under
26+
* WSL (`/mnt/<x>/...`), Cygwin (`/cygdrive/<x>/...`), or
27+
* MSYS (`/<x>/...`) get rewritten to the Windows drive
28+
* form `<x>:/...`.
29+
*
30+
* Each entry's prefix and the next byte after it must agree: the
31+
* byte at position `prefix_len` must be a single ASCII letter and
32+
* the byte after that must be `/`, `\`, or end-of-string. That
33+
* separator check is what stops `/mnt/storage` and `/c/foo` (no
34+
* such drive `/` `c`) from being misread when the drive segment
35+
* is multi-character.
36+
*/
37+
static const struct {
38+
const char *prefix;
39+
size_t prefix_len;
40+
} posix_prefixes[] = {
41+
{ "/mnt/", 5 }, /* WSL */
42+
{ "/cygdrive/", 10 }, /* Cygwin */
43+
{ "/", 1 }, /* MSYS */
44+
};
45+
size_t i;
46+
47+
if (path->len == 0)
48+
return 0;
49+
50+
for (i = 0; i < ARRAY_SIZE(posix_prefixes); i++) {
51+
size_t pl = posix_prefixes[i].prefix_len;
52+
char drive;
53+
54+
if (path->len < pl + 1)
55+
continue;
56+
if (memcmp(path->buf, posix_prefixes[i].prefix, pl) != 0)
57+
continue;
58+
drive = path->buf[pl];
59+
if (!isalpha((unsigned char)drive))
60+
continue;
61+
if (path->len > pl + 1 && path->buf[pl + 1] != '/' && path->buf[pl + 1] != '\\')
62+
continue;
63+
64+
/* "<prefix><drive>" (pl+1 bytes) -> "<drive>:" (2 bytes). */
65+
path->buf[0] = drive;
66+
path->buf[1] = ':';
67+
memmove(path->buf + 2, path->buf + pl + 1, path->len - pl);
68+
strbuf_setlen(path, path->len - (pl - 1));
69+
return 1;
70+
}
71+
return 0;
72+
#else
73+
/*
74+
* POSIX direction: paths recorded by Windows git (`<x>:/...` or
75+
* `<x>:\...`) get rewritten to the mount form used by this build:
76+
*
77+
* - MSYS2 runtime: /<x>/...
78+
* - real Cygwin: /cygdrive/<x>/...
79+
* - everything else: /mnt/<x>/... (WSL2 default; harmless
80+
* elsewhere because the
81+
* resulting path simply
82+
* does not resolve)
83+
*
84+
* Backslashes in the remainder are converted to forward slashes.
85+
* May grow the buffer; on MSYS2 the result is the same length as
86+
* the input.
87+
*/
88+
#if defined(__MSYS__)
89+
static const char drive_prefix[] = "/";
90+
#elif defined(__CYGWIN__)
2591
static const char drive_prefix[] = "/cygdrive/";
2692
#else
2793
static const char drive_prefix[] = "/mnt/";
@@ -42,8 +108,6 @@ int translate_windows_path(struct strbuf *path)
42108

43109
drive = tolower((unsigned char)path->buf[0]);
44110

45-
/* Rewrite "<letter>:" as "<drive_prefix><drive>", then convert any
46-
* backslashes in the remaining path to forward slashes. */
47111
strbuf_grow(path, expansion);
48112
memmove(path->buf + 2 + expansion, path->buf + 2, path->len - 2 + 1);
49113
memcpy(path->buf, drive_prefix, drive_prefix_len);
@@ -55,9 +119,6 @@ int translate_windows_path(struct strbuf *path)
55119
path->buf[i] = '/';
56120
}
57121
return 1;
58-
#else
59-
(void)path;
60-
return 0;
61122
#endif
62123
}
63124

path.h

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,30 @@ struct string_list;
77
struct worktree;
88

99
/*
10-
* Translate Windows-style absolute paths (`<x>:/...` or `<x>:\...`) recorded
11-
* by git running on native Windows into the POSIX mount form used by the
12-
* current build:
10+
* Translate worktree gitdir paths between native Windows form and the
11+
* POSIX mount form used by WSL/Cygwin/MSYS, in whichever direction is
12+
* appropriate for the current build:
1313
*
14-
* * Cygwin / MSYS: `/cygdrive/<x>/...`
15-
* * everything else: `/mnt/<x>/...` (suits WSL2, harmless elsewhere)
14+
* * On a `GIT_WINDOWS_NATIVE` build (i.e. native Windows git), paths
15+
* beginning with `/mnt/<x>/` (WSL), `/cygdrive/<x>/` (Cygwin),
16+
* or `/<x>/` (MSYS) are rewritten to the Windows drive form
17+
* `<x>:/...`.
1618
*
17-
* Edits `path` in place; the strbuf may grow. Backslashes in the remainder
18-
* are converted to forward slashes. Returns 1 if a translation occurred,
19-
* 0 otherwise.
19+
* * On any other build, paths beginning with `<x>:/` or `<x>:\`
20+
* (recorded by git running on native Windows) are rewritten to the
21+
* mount form used by this runtime: `/<x>/...` on MSYS2,
22+
* `/cygdrive/<x>/...` on real Cygwin, `/mnt/<x>/...` everywhere
23+
* else (WSL2 default; harmless elsewhere because the resulting
24+
* path simply does not resolve). Backslashes in the remainder are
25+
* normalised to forward slashes.
2026
*
21-
* No-op on native Windows builds, where the input is already in the native
22-
* form.
27+
* `<x>` must be a single ASCII letter; multi-character segments such as
28+
* `/mnt/storage` and digit-prefixed mounts pass through unchanged so
29+
* legitimate Linux paths are never disturbed.
30+
*
31+
* Edits `path` in place; the strbuf may shrink (Windows direction) or
32+
* grow (POSIX direction). Returns 1 if a translation occurred, 0
33+
* otherwise.
2334
*/
2435
int translate_windows_path(struct strbuf *path);
2536

setup.c

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,7 +1010,6 @@ const char *read_gitfile_gently(const char *path, int *return_error_code)
10101010
buf[len] = '\0';
10111011
dir = buf + 8;
10121012

1013-
#ifndef GIT_WINDOWS_NATIVE
10141013
{
10151014
struct strbuf translated = STRBUF_INIT;
10161015
strbuf_addstr(&translated, dir);
@@ -1023,7 +1022,6 @@ const char *read_gitfile_gently(const char *path, int *return_error_code)
10231022
}
10241023
strbuf_release(&translated);
10251024
}
1026-
#endif
10271025

10281026
if (!is_absolute_path(dir) && (slash = strrchr(path, '/'))) {
10291027
size_t pathlen = slash+1 - path;

t/t0042-wsl-mnt-path.sh

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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 any drive-prefixed path Windows git might emit to the named
17+
# POSIX-mount form. Handles MSYS form (/c/foo) and Windows forms
18+
# (C:/foo and C:\foo). MINGW-only.
19+
mount_form () {
20+
prefix=$1 ;# /mnt or /cygdrive
21+
path=$2
22+
case "$path" in
23+
/[A-Za-z]/*)
24+
echo "$path" | sed -E "s|^/([A-Za-z])/|$prefix/\\L\\1/|"
25+
;;
26+
[A-Za-z]:/*)
27+
echo "$path" | sed -E "s|^([A-Za-z]):/|$prefix/\\L\\1/|"
28+
;;
29+
[A-Za-z]:'\'*)
30+
echo "$path" | sed -E "s|^([A-Za-z]):.|$prefix/\\L\\1/|; s|\\\\|/|g"
31+
;;
32+
*)
33+
echo "$path"
34+
;;
35+
esac
36+
}
37+
38+
to_mnt () {
39+
mount_form /mnt "$1"
40+
}
41+
42+
to_cygdrive () {
43+
mount_form /cygdrive "$1"
44+
}
45+
46+
# Convert any drive-prefixed path to the MSYS2 form (/c/foo). MINGW-only.
47+
to_msys () {
48+
case "$1" in
49+
/[A-Za-z]/*)
50+
# Already MSYS form.
51+
echo "$1"
52+
;;
53+
[A-Za-z]:/*)
54+
echo "$1" | sed -E "s|^([A-Za-z]):/|/\\L\\1/|"
55+
;;
56+
[A-Za-z]:'\'*)
57+
echo "$1" | sed -E "s|^([A-Za-z]):.|/\\L\\1/|; s|\\\\|/|g"
58+
;;
59+
*)
60+
echo "$1"
61+
;;
62+
esac
63+
}
64+
65+
test_expect_success MINGW 'setup main repo' '
66+
git init repo &&
67+
test_commit -C repo init
68+
'
69+
70+
test_expect_success MINGW 'read_gitfile_gently translates /mnt/<x>/ gitdir' '
71+
test_when_finished "rm -rf wtlink actual" &&
72+
REAL=$(cd repo/.git && pwd) &&
73+
MNT=$(to_mnt "$REAL") &&
74+
75+
# Sanity: the path must actually start with /mnt/ - if it does not,
76+
# the host shell did not give us a path with a drive prefix and the
77+
# rest of the test would be silently meaningless.
78+
case "$MNT" in
79+
/mnt/*) : ok ;;
80+
*) BUG "to_mnt produced $MNT from $REAL" ;;
81+
esac &&
82+
83+
mkdir wtlink &&
84+
printf "gitdir: %s\n" "$MNT" >wtlink/.git &&
85+
86+
(cd wtlink && git rev-parse --git-dir) >actual &&
87+
test_path_is_dir "$(cat actual)"
88+
'
89+
90+
test_expect_success MINGW 'read_gitfile_gently translates /cygdrive/<x>/ gitdir' '
91+
test_when_finished "rm -rf wtlink actual" &&
92+
REAL=$(cd repo/.git && pwd) &&
93+
CYG=$(to_cygdrive "$REAL") &&
94+
95+
mkdir wtlink &&
96+
printf "gitdir: %s\n" "$CYG" >wtlink/.git &&
97+
98+
(cd wtlink && git rev-parse --git-dir) >actual &&
99+
test_path_is_dir "$(cat actual)"
100+
'
101+
102+
test_expect_success MINGW 'read_gitfile_gently translates /<x>/ MSYS2 gitdir' '
103+
test_when_finished "rm -rf wtlink actual" &&
104+
REAL=$(cd repo/.git && pwd) &&
105+
MSYS_PATH=$(to_msys "$REAL") &&
106+
107+
case "$MSYS_PATH" in
108+
/[A-Za-z]/*) : ok ;;
109+
*) BUG "to_msys produced $MSYS_PATH from $REAL" ;;
110+
esac &&
111+
112+
mkdir wtlink &&
113+
printf "gitdir: %s\n" "$MSYS_PATH" >wtlink/.git &&
114+
115+
(cd wtlink && git rev-parse --git-dir) >actual &&
116+
test_path_is_dir "$(cat actual)"
117+
'
118+
119+
test_expect_success MINGW 'read_gitfile_gently leaves /mnt/<multichar>/ alone' '
120+
test_when_finished "rm -rf wtlink" &&
121+
mkdir wtlink &&
122+
# "storage" is not a single drive letter, so this must not be
123+
# translated. The path does not exist on Windows, so the open fails.
124+
echo "gitdir: /mnt/storage/no/such/repo" >wtlink/.git &&
125+
126+
test_must_fail git -C wtlink rev-parse --git-dir 2>err &&
127+
test_grep "not a git repository" err
128+
'
129+
130+
test_expect_success MINGW 'get_linked_worktree finds worktree recorded with /mnt/<x>/ path' '
131+
test_when_finished "rm -rf repo/wt repo/.git/worktrees/wt" &&
132+
133+
git -C repo worktree add --detach wt &&
134+
WT_REAL=$(cd repo/wt && pwd) &&
135+
WT_MNT=$(to_mnt "$WT_REAL") &&
136+
137+
# Overwrite the recorded worktree path with the WSL form, mimicking
138+
# what `git worktree add` writes when run from inside WSL.
139+
printf "%s/.git\n" "$WT_MNT" >repo/.git/worktrees/wt/gitdir &&
140+
141+
# `git worktree list` reads that file via get_linked_worktree.
142+
# After translation the worktree must still be reachable: it must
143+
# NOT be flagged prunable, and a git operation inside the worktree
144+
# directory must succeed.
145+
git -C repo worktree list --porcelain >list &&
146+
! grep -q "^prunable" list &&
147+
(cd "$WT_REAL" && git rev-parse --is-inside-work-tree)
148+
'
149+
150+
test_expect_success MINGW 'get_common_dir_noenv translates /mnt/<x>/ commondir' '
151+
test_when_finished "rm -rf wtdir wt actual" &&
152+
153+
REAL=$(cd repo/.git && pwd) &&
154+
MNT=$(to_mnt "$REAL") &&
155+
156+
# Build a synthetic linked-worktree gitdir that points at the main
157+
# repo via a /mnt/<x>/ commondir record.
158+
mkdir wtdir &&
159+
echo "$(cd repo && git rev-parse HEAD)" >wtdir/HEAD &&
160+
echo "$MNT" >wtdir/commondir &&
161+
printf "%s/.git\n" "$(pwd)" >wtdir/gitdir &&
162+
163+
# rev-parse --git-common-dir on a checkout that points here should
164+
# resolve through the translated commondir.
165+
mkdir wt &&
166+
printf "gitdir: %s\n" "$(pwd)/wtdir" >wt/.git &&
167+
(cd wt && git rev-parse --git-common-dir) >actual &&
168+
test_path_is_dir "$(cat actual)"
169+
'
170+
171+
test_done

t/t0060-path-utils.sh

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -612,14 +612,14 @@ test_expect_success !VALGRIND,RUNTIME_PREFIX,CAN_EXEC_IN_PWD '%(prefix)/ works'
612612
'
613613

614614
# translate_windows_path is a no-op when git is built for native Windows
615-
# (the input is already in the native form). On Cygwin the mount root is
616-
# /cygdrive/<x>; everywhere else it is /mnt/<x>.
617-
if test_have_prereq CYGWIN
618-
then
619-
WSL_DRIVE_PREFIX=/cygdrive
620-
else
621-
WSL_DRIVE_PREFIX=/mnt
622-
fi
615+
# (the input is already in the native form). The expected mount root
616+
# depends on the current build's runtime: /<x>/ on MSYS2, /cygdrive/<x>/
617+
# on real Cygwin, /mnt/<x>/ everywhere else (WSL2 default).
618+
case "$(uname -s)" in
619+
*CYGWIN*) WSL_DRIVE_PREFIX=/cygdrive ;;
620+
*MSYS_NT*) WSL_DRIVE_PREFIX= ;;
621+
*) WSL_DRIVE_PREFIX=/mnt ;;
622+
esac
623623

624624
translate_windows_path() {
625625
test_expect_success !MINGW "translate_windows_path: $1 => $2" "

worktree.c

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ 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 by git running on Windows. */
158+
/* Worktree path may have been recorded by Windows or WSL/Cygwin git. */
159159
translate_windows_path(&worktree_path);
160160

161161
if (!is_absolute_path(worktree_path.buf)) {
@@ -996,7 +996,6 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath,
996996
}
997997
path[len] = '\0';
998998

999-
#ifndef GIT_WINDOWS_NATIVE
1000999
{
10011000
struct strbuf translated = STRBUF_INIT;
10021001
strbuf_addstr(&translated, path);
@@ -1007,7 +1006,6 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath,
10071006
strbuf_release(&translated);
10081007
}
10091008
}
1010-
#endif
10111009

10121010
if (is_absolute_path(path)) {
10131011
strbuf_addstr(&dotgit, path);

0 commit comments

Comments
 (0)