Skip to content

Commit 0324283

Browse files
committed
path.c: translate Windows paths recorded by Windows git on POSIX hosts
When `git worktree add` is run from native Windows, git writes absolute paths into the worktree's `.git` file, into `<commondir>/worktrees/<id>/gitdir`, and (when present) into `<commondir>/commondir`, in `<x>:/...` or `<x>:\...` form. Reading those files back from a non-Windows-native build of git fails because neither form is meaningful on POSIX, so the worktree appears broken even though every byte of it is reachable - the most common scenario being a worktree on a Windows drive opened from inside WSL2 (where the Windows filesystem is mounted at `/mnt/<x>/`) or from Cygwin/MSYS (where it is `/cygdrive/<x>/`). Add a small helper `translate_windows_path()` that recognises this shape at the start of a path and rewrites it to the POSIX mount form appropriate for the current build (`/cygdrive/<x>/` on Cygwin, `/mnt/<x>/` everywhere else), converting any backslashes in the remainder to forward slashes. Call it at the three places where non-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 `<x>:/` or `<x>:\` where `<x>` is a single ASCII letter; anything else is left alone. The helper is a no-op on `GIT_WINDOWS_NATIVE` builds, where the input is already in native form. On non-WSL Linux hosts the translation still produces a syntactically valid POSIX path; if the corresponding `/mnt/<x>/` mount does not exist, the next stat()/open() fails as it would have without translation - i.e. the change cannot make a working configuration stop working. Add a `translate_windows_path` subcommand to the path-utils test tool and cover it in `t/t0060-path-utils.sh`. The test fixtures pick the expected prefix from the CYGWIN prereq so the same suite passes on Linux and Cygwin builds. Signed-off-by: johnnyshields <27655+johnnyshields@users.noreply.github.com>
1 parent 94f0577 commit 0324283

6 files changed

Lines changed: 120 additions & 0 deletions

File tree

path.c

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

21+
int translate_windows_path(struct strbuf *path)
22+
{
23+
#ifndef GIT_WINDOWS_NATIVE
24+
#ifdef __CYGWIN__
25+
static const char drive_prefix[] = "/cygdrive/";
26+
#else
27+
static const char drive_prefix[] = "/mnt/";
28+
#endif
29+
const size_t drive_prefix_len = sizeof(drive_prefix) - 1;
30+
const size_t expansion = drive_prefix_len + 1 - 2;
31+
char drive;
32+
size_t i;
33+
34+
if (path->len < 3)
35+
return 0;
36+
if (!isalpha((unsigned char)path->buf[0]))
37+
return 0;
38+
if (path->buf[1] != ':')
39+
return 0;
40+
if (path->buf[2] != '/' && path->buf[2] != '\\')
41+
return 0;
42+
43+
drive = tolower((unsigned char)path->buf[0]);
44+
45+
/* Rewrite "<letter>:" as "<drive_prefix><drive>", then convert any
46+
* backslashes in the remaining path to forward slashes. */
47+
strbuf_grow(path, expansion);
48+
memmove(path->buf + 2 + expansion, path->buf + 2, path->len - 2 + 1);
49+
memcpy(path->buf, drive_prefix, drive_prefix_len);
50+
path->buf[drive_prefix_len] = drive;
51+
path->len += expansion;
52+
53+
for (i = drive_prefix_len + 1; i < path->len; i++) {
54+
if (path->buf[i] == '\\')
55+
path->buf[i] = '/';
56+
}
57+
return 1;
58+
#else
59+
(void)path;
60+
return 0;
61+
#endif
62+
}
63+
2164
static int get_st_mode_bits(const char *path, int *mode)
2265
{
2366
struct stat st;

path.h

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

9+
/*
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:
13+
*
14+
* * Cygwin / MSYS: `/cygdrive/<x>/...`
15+
* * everything else: `/mnt/<x>/...` (suits WSL2, harmless elsewhere)
16+
*
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.
20+
*
21+
* No-op on native Windows builds, where the input is already in the native
22+
* form.
23+
*/
24+
int translate_windows_path(struct strbuf *path);
25+
926
/*
1027
* The result to all functions which return statically allocated memory may be
1128
* overwritten by another call to _any_ one of these functions. Consider using

setup.c

Lines changed: 16 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_windows_path(&data);
339340
if (!is_absolute_path(data.buf))
340341
strbuf_addf(&path, "%s/", gitdir);
341342
strbuf_addbuf(&path, &data);
@@ -1009,6 +1010,21 @@ const char *read_gitfile_gently(const char *path, int *return_error_code)
10091010
buf[len] = '\0';
10101011
dir = buf + 8;
10111012

1013+
#ifndef GIT_WINDOWS_NATIVE
1014+
{
1015+
struct strbuf translated = STRBUF_INIT;
1016+
strbuf_addstr(&translated, dir);
1017+
if (translate_windows_path(&translated)) {
1018+
char *new_buf = xstrfmt("gitdir: %s", translated.buf);
1019+
free(buf);
1020+
buf = new_buf;
1021+
len = strlen(buf);
1022+
dir = buf + 8;
1023+
}
1024+
strbuf_release(&translated);
1025+
}
1026+
#endif
1027+
10121028
if (!is_absolute_path(dir) && (slash = strrchr(path, '/'))) {
10131029
size_t pathlen = slash+1 - path;
10141030
dir = xstrfmt("%.*s%.*s", (int)pathlen, path,

t/helper/test-path-utils.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,15 @@ int cmd__path_utils(int argc, const char **argv)
401401
return 0;
402402
}
403403

404+
if (argc == 3 && !strcmp(argv[1], "translate_windows_path")) {
405+
struct strbuf sb = STRBUF_INIT;
406+
strbuf_addstr(&sb, argv[2]);
407+
translate_windows_path(&sb);
408+
puts(sb.buf);
409+
strbuf_release(&sb);
410+
return 0;
411+
}
412+
404413
if (argc == 4 && !strcmp(argv[1], "relative_path")) {
405414
struct strbuf sb = STRBUF_INIT;
406415
const char *in, *prefix, *rel;

t/t0060-path-utils.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,4 +611,36 @@ 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
623+
624+
translate_windows_path() {
625+
test_expect_success !MINGW "translate_windows_path: $1 => $2" "
626+
echo '$2' >expect &&
627+
test-tool path-utils translate_windows_path '$1' >actual &&
628+
test_cmp expect actual
629+
"
630+
}
631+
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"
637+
638+
# Inputs that must NOT be translated:
639+
translate_windows_path '/already/posix' '/already/posix'
640+
translate_windows_path 'relative/path' 'relative/path'
641+
translate_windows_path 'C:relative-no-separator' 'C:relative-no-separator'
642+
translate_windows_path '1:/digit-prefix' '1:/digit-prefix'
643+
translate_windows_path 'C' 'C'
644+
translate_windows_path '' ''
645+
614646
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 by git running on Windows. */
159+
translate_windows_path(&worktree_path);
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)