Skip to content

Commit 84281bf

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 84281bf

6 files changed

Lines changed: 223 additions & 36 deletions

File tree

path.c

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,57 @@
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+
* Windows-native: rewrite POSIX-mount paths (WSL `/mnt/<x>/`,
26+
* Cygwin `/cygdrive/<x>/`, MSYS `/<x>/`) to drive form `<x>:/...`.
27+
* The single-letter + separator check below is what stops
28+
* multi-character segments like `/mnt/storage` from matching.
29+
*/
30+
static const struct {
31+
const char *prefix;
32+
size_t prefix_len;
33+
} posix_prefixes[] = {
34+
{ "/mnt/", 5 }, /* WSL */
35+
{ "/cygdrive/", 10 }, /* Cygwin */
36+
{ "/", 1 }, /* MSYS */
37+
};
38+
size_t i;
39+
40+
if (path->len == 0)
41+
return 0;
42+
43+
for (i = 0; i < ARRAY_SIZE(posix_prefixes); i++) {
44+
size_t pl = posix_prefixes[i].prefix_len;
45+
char drive;
46+
47+
if (path->len < pl + 1)
48+
continue;
49+
if (memcmp(path->buf, posix_prefixes[i].prefix, pl) != 0)
50+
continue;
51+
drive = path->buf[pl];
52+
if (!isalpha((unsigned char)drive))
53+
continue;
54+
if (path->len > pl + 1 && path->buf[pl + 1] != '/' && path->buf[pl + 1] != '\\')
55+
continue;
56+
57+
/* "<prefix><drive>" (pl+1 bytes) -> "<drive>:" (2 bytes). */
58+
path->buf[0] = drive;
59+
path->buf[1] = ':';
60+
memmove(path->buf + 2, path->buf + pl + 1, path->len - pl);
61+
strbuf_setlen(path, path->len - (pl - 1));
62+
return 1;
63+
}
64+
return 0;
65+
#else
66+
/*
67+
* POSIX: rewrite Windows-form `<x>:/...` or `<x>:\...` to this
68+
* build's mount form (drive_prefix selected below), normalising
69+
* any backslashes in the tail.
70+
*/
71+
#if defined(__MSYS__)
72+
static const char drive_prefix[] = "/";
73+
#elif defined(__CYGWIN__)
2574
static const char drive_prefix[] = "/cygdrive/";
2675
#else
2776
static const char drive_prefix[] = "/mnt/";
@@ -42,8 +91,6 @@ int translate_windows_path(struct strbuf *path)
4291

4392
drive = tolower((unsigned char)path->buf[0]);
4493

45-
/* Rewrite "<letter>:" as "<drive_prefix><drive>", then convert any
46-
* backslashes in the remaining path to forward slashes. */
4794
strbuf_grow(path, expansion);
4895
memmove(path->buf + 2 + expansion, path->buf + 2, path->len - 2 + 1);
4996
memcpy(path->buf, drive_prefix, drive_prefix_len);
@@ -55,9 +102,6 @@ int translate_windows_path(struct strbuf *path)
55102
path->buf[i] = '/';
56103
}
57104
return 1;
58-
#else
59-
(void)path;
60-
return 0;
61105
#endif
62106
}
63107

path.h

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,21 @@ 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 and POSIX-mount
11+
* forms, in the direction appropriate for the current build:
1312
*
14-
* * Cygwin / MSYS: `/cygdrive/<x>/...`
15-
* * everything else: `/mnt/<x>/...` (suits WSL2, harmless elsewhere)
13+
* * Windows-native: `/mnt/<x>/`, `/cygdrive/<x>/`, or `/<x>/` -> `<x>:/...`
14+
* * MSYS: `<x>:/` or `<x>:\` -> `/<x>/...`
15+
* * Cygwin: `<x>:/` or `<x>:\` -> `/cygdrive/<x>/...`
16+
* * everything else (Linux/WSL/macOS/...): `<x>:/` or `<x>:\` -> `/mnt/<x>/...`
1617
*
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.
18+
* `<x>` must be a single ASCII letter; multi-character segments
19+
* (`/mnt/storage`) and digit-prefixed mounts pass through unchanged.
20+
* Backslashes in the tail are normalised to forward slashes on the
21+
* POSIX-direction translation.
2022
*
21-
* No-op on native Windows builds, where the input is already in the native
22-
* form.
23+
* Edits `path` in place; may shrink (Windows direction) or grow (POSIX
24+
* direction). Returns 1 if a translation occurred, 0 otherwise.
2325
*/
2426
int translate_windows_path(struct strbuf *path);
2527

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

t/t0060-path-utils.sh

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -611,15 +611,11 @@ test_expect_success !VALGRIND,RUNTIME_PREFIX,CAN_EXEC_IN_PWD '%(prefix)/ works'
611611
test_cmp expect actual
612612
'
613613

614-
# 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
614+
case "$(uname -s)" in
615+
*CYGWIN*) MOUNT_PREFIX=/cygdrive ;;
616+
*MSYS_NT*) MOUNT_PREFIX= ;;
617+
*) MOUNT_PREFIX=/mnt ;;
618+
esac
623619

624620
translate_windows_path() {
625621
test_expect_success !MINGW "translate_windows_path: $1 => $2" "
@@ -629,11 +625,11 @@ translate_windows_path() {
629625
"
630626
}
631627

632-
translate_windows_path 'C:/foo/bar' "$WSL_DRIVE_PREFIX/c/foo/bar"
633-
translate_windows_path 'C:\foo\bar' "$WSL_DRIVE_PREFIX/c/foo/bar"
634-
translate_windows_path 'D:/repo/.git/worktrees/wt' "$WSL_DRIVE_PREFIX/d/repo/.git/worktrees/wt"
635-
translate_windows_path 'Z:\path\with mixed/seps' "$WSL_DRIVE_PREFIX/z/path/with mixed/seps"
636-
translate_windows_path 'c:/already-lower' "$WSL_DRIVE_PREFIX/c/already-lower"
628+
translate_windows_path 'C:/foo/bar' "$MOUNT_PREFIX/c/foo/bar"
629+
translate_windows_path 'C:\foo\bar' "$MOUNT_PREFIX/c/foo/bar"
630+
translate_windows_path 'D:/repo/.git/worktrees/wt' "$MOUNT_PREFIX/d/repo/.git/worktrees/wt"
631+
translate_windows_path 'Z:\path\with mixed/seps' "$MOUNT_PREFIX/z/path/with mixed/seps"
632+
translate_windows_path 'c:/already-lower' "$MOUNT_PREFIX/c/already-lower"
637633

638634
# Inputs that must NOT be translated:
639635
translate_windows_path '/already/posix' '/already/posix'

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+
/* Convert Windows path to POSIX path or vice-versa. */
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)