Skip to content

Commit 766de21

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 766de21

6 files changed

Lines changed: 223 additions & 30 deletions

File tree

path.c

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,65 @@
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+
* WSL2 (`/mnt/<x>/...`) or Cygwin/MSYS (`/cygdrive/<x>/...`) get
27+
* rewritten to the Windows drive form `<x>:/...`. Shrinks in place.
28+
*/
29+
static const struct {
30+
const char *prefix;
31+
size_t prefix_len;
32+
} posix_prefixes[] = {
33+
{ "/mnt/", 5 },
34+
{ "/cygdrive/", 10 },
35+
};
36+
size_t i;
37+
38+
if (path->len == 0)
39+
return 0;
40+
41+
for (i = 0; i < ARRAY_SIZE(posix_prefixes); i++) {
42+
size_t pl = posix_prefixes[i].prefix_len;
43+
char drive;
44+
45+
if (path->len < pl + 1)
46+
continue;
47+
if (memcmp(path->buf, posix_prefixes[i].prefix, pl) != 0)
48+
continue;
49+
drive = path->buf[pl];
50+
if (!isalpha((unsigned char)drive))
51+
continue;
52+
if (path->len > pl + 1 && path->buf[pl + 1] != '/' && path->buf[pl + 1] != '\\')
53+
continue;
54+
55+
/* "<prefix><drive>" (pl+1 bytes) -> "<drive>:" (2 bytes). */
56+
path->buf[0] = drive;
57+
path->buf[1] = ':';
58+
memmove(path->buf + 2, path->buf + pl + 1, path->len - pl);
59+
strbuf_setlen(path, path->len - (pl - 1));
60+
return 1;
61+
}
62+
return 0;
63+
#else
64+
/*
65+
* POSIX direction: paths recorded by Windows git (`<x>:/...` or
66+
* `<x>:\...`) get rewritten to the mount form used by this build:
67+
*
68+
* - MSYS2 runtime: /<x>/...
69+
* - real Cygwin: /cygdrive/<x>/...
70+
* - everything else: /mnt/<x>/... (WSL2 default; harmless
71+
* elsewhere because the
72+
* resulting path simply
73+
* does not resolve)
74+
*
75+
* Backslashes in the remainder are converted to forward slashes.
76+
* May grow the buffer; on MSYS2 the result is the same length as
77+
* the input.
78+
*/
79+
#if defined(__MSYS__)
80+
static const char drive_prefix[] = "/";
81+
#elif defined(__CYGWIN__)
2582
static const char drive_prefix[] = "/cygdrive/";
2683
#else
2784
static const char drive_prefix[] = "/mnt/";
@@ -42,8 +99,6 @@ int translate_windows_path(struct strbuf *path)
4299

43100
drive = tolower((unsigned char)path->buf[0]);
44101

45-
/* Rewrite "<letter>:" as "<drive_prefix><drive>", then convert any
46-
* backslashes in the remaining path to forward slashes. */
47102
strbuf_grow(path, expansion);
48103
memmove(path->buf + 2 + expansion, path->buf + 2, path->len - 2 + 1);
49104
memcpy(path->buf, drive_prefix, drive_prefix_len);
@@ -55,9 +110,6 @@ int translate_windows_path(struct strbuf *path)
55110
path->buf[i] = '/';
56111
}
57112
return 1;
58-
#else
59-
(void)path;
60-
return 0;
61113
#endif
62114
}
63115

path.h

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,29 @@ 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>/` (WSL2) or `/cygdrive/<x>/` (Cygwin/MSYS)
16+
* are rewritten to the Windows drive form `<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+
* * On any other build, paths beginning with `<x>:/` or `<x>:\`
19+
* (recorded by git running on native Windows) are rewritten to the
20+
* mount form used by this runtime: `/<x>/...` on MSYS2,
21+
* `/cygdrive/<x>/...` on real Cygwin, `/mnt/<x>/...` everywhere
22+
* else (WSL2 default; harmless elsewhere because the resulting
23+
* path simply does not resolve). Backslashes in the remainder are
24+
* normalised to forward slashes.
2025
*
21-
* No-op on native Windows builds, where the input is already in the native
22-
* form.
26+
* `<x>` must be a single ASCII letter; multi-character segments such as
27+
* `/mnt/storage` and digit-prefixed mounts pass through unchanged so
28+
* legitimate Linux paths are never disturbed.
29+
*
30+
* Edits `path` in place; the strbuf may shrink (Windows direction) or
31+
* grow (POSIX direction). Returns 1 if a translation occurred, 0
32+
* otherwise.
2333
*/
2434
int translate_windows_path(struct strbuf *path);
2535

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: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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+
test_expect_success MINGW 'setup main repo' '
47+
git init repo &&
48+
test_commit -C repo init
49+
'
50+
51+
test_expect_success MINGW 'read_gitfile_gently translates /mnt/<x>/ gitdir' '
52+
test_when_finished "rm -rf wtlink actual" &&
53+
REAL=$(cd repo/.git && pwd) &&
54+
MNT=$(to_mnt "$REAL") &&
55+
56+
# Sanity: the path must actually start with /mnt/ - if it does not,
57+
# the host shell did not give us a path with a drive prefix and the
58+
# rest of the test would be silently meaningless.
59+
case "$MNT" in
60+
/mnt/*) : ok ;;
61+
*) BUG "to_mnt produced $MNT from $REAL" ;;
62+
esac &&
63+
64+
mkdir wtlink &&
65+
printf "gitdir: %s\n" "$MNT" >wtlink/.git &&
66+
67+
(cd wtlink && git rev-parse --git-dir) >actual &&
68+
test_path_is_dir "$(cat actual)"
69+
'
70+
71+
test_expect_success MINGW 'read_gitfile_gently translates /cygdrive/<x>/ gitdir' '
72+
test_when_finished "rm -rf wtlink actual" &&
73+
REAL=$(cd repo/.git && pwd) &&
74+
CYG=$(to_cygdrive "$REAL") &&
75+
76+
mkdir wtlink &&
77+
printf "gitdir: %s\n" "$CYG" >wtlink/.git &&
78+
79+
(cd wtlink && git rev-parse --git-dir) >actual &&
80+
test_path_is_dir "$(cat actual)"
81+
'
82+
83+
test_expect_success MINGW 'read_gitfile_gently leaves /mnt/<multichar>/ alone' '
84+
test_when_finished "rm -rf wtlink" &&
85+
mkdir wtlink &&
86+
# "storage" is not a single drive letter, so this must not be
87+
# translated. The path does not exist on Windows, so the open fails.
88+
echo "gitdir: /mnt/storage/no/such/repo" >wtlink/.git &&
89+
90+
test_must_fail git -C wtlink rev-parse --git-dir 2>err &&
91+
test_grep "not a git repository" err
92+
'
93+
94+
test_expect_success MINGW 'get_linked_worktree finds worktree recorded with /mnt/<x>/ path' '
95+
test_when_finished "rm -rf repo/wt repo/.git/worktrees/wt" &&
96+
97+
git -C repo worktree add --detach wt &&
98+
WT_REAL=$(cd repo/wt && pwd) &&
99+
WT_MNT=$(to_mnt "$WT_REAL") &&
100+
101+
# Overwrite the recorded worktree path with the WSL form, mimicking
102+
# what `git worktree add` writes when run from inside WSL.
103+
printf "%s/.git\n" "$WT_MNT" >repo/.git/worktrees/wt/gitdir &&
104+
105+
# `git worktree list` reads that file via get_linked_worktree.
106+
# After translation the worktree must still be reachable: it must
107+
# NOT be flagged prunable, and a git operation inside the worktree
108+
# directory must succeed.
109+
git -C repo worktree list --porcelain >list &&
110+
! grep -q "^prunable" list &&
111+
(cd "$WT_REAL" && git rev-parse --is-inside-work-tree)
112+
'
113+
114+
test_expect_success MINGW 'get_common_dir_noenv translates /mnt/<x>/ commondir' '
115+
test_when_finished "rm -rf wtdir wt actual" &&
116+
117+
REAL=$(cd repo/.git && pwd) &&
118+
MNT=$(to_mnt "$REAL") &&
119+
120+
# Build a synthetic linked-worktree gitdir that points at the main
121+
# repo via a /mnt/<x>/ commondir record.
122+
mkdir wtdir &&
123+
echo "$(cd repo && git rev-parse HEAD)" >wtdir/HEAD &&
124+
echo "$MNT" >wtdir/commondir &&
125+
printf "%s/.git\n" "$(pwd)" >wtdir/gitdir &&
126+
127+
# rev-parse --git-common-dir on a checkout that points here should
128+
# resolve through the translated commondir.
129+
mkdir wt &&
130+
printf "gitdir: %s\n" "$(pwd)/wtdir" >wt/.git &&
131+
(cd wt && git rev-parse --git-common-dir) >actual &&
132+
test_path_is_dir "$(cat actual)"
133+
'
134+
135+
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)