From af0a7b2f25b92082c5851216bea19094c7dc7570 Mon Sep 17 00:00:00 2001 From: johnnyshields <27655+johnnyshields@users.noreply.github.com> Date: Thu, 7 May 2026 00:00:00 +0000 Subject: [PATCH] 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 `/worktrees//`, which points at the worktree's `.git` link. Translation only happens for `/mnt//` and `/cygdrive//` where `` 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> --- path.c | 39 ++++++++++++ path.h | 9 +++ setup.c | 3 + t/t0042-wsl-mnt-path.sh | 135 ++++++++++++++++++++++++++++++++++++++++ worktree.c | 5 ++ 5 files changed, 191 insertions(+) create mode 100644 t/t0042-wsl-mnt-path.sh diff --git a/path.c b/path.c index f45263210986cb..49e42887328f06 100644 --- a/path.c +++ b/path.c @@ -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 }, + }; + 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 ":" + 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; diff --git a/path.h b/path.h index 85713809f63624..087ba0fa82f459 100644 --- a/path.h +++ b/path.h @@ -6,6 +6,15 @@ struct strbuf; struct string_list; struct worktree; +/* + * Translate POSIX-style drive paths produced by Git running under WSL2 + * (`/mnt//...`) or Cygwin/MSYS (`/cygdrive//...`) into Windows + * drive-letter form (`:/...`). 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 diff --git a/setup.c b/setup.c index 55f45345b4a2b5..4a00ec40c55eca 100644 --- a/setup.c +++ b/setup.c @@ -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); @@ -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, diff --git a/t/t0042-wsl-mnt-path.sh b/t/t0042-wsl-mnt-path.sh new file mode 100644 index 00000000000000..40e1546306159a --- /dev/null +++ b/t/t0042-wsl-mnt-path.sh @@ -0,0 +1,135 @@ +#!/bin/sh + +test_description='translate WSL/Cygwin /mnt// 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// 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// 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// 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// 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// 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// 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 diff --git a/worktree.c b/worktree.c index d874e23b4e158a..9178ec59479977 100644 --- a/worktree.c +++ b/worktree.c @@ -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); @@ -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 {