Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions path.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,93 @@
#include "lockfile.h"
#include "exec-cmd.h"

int translate_windows_path(struct strbuf *path)
{
#ifdef GIT_WINDOWS_NATIVE
/*
* Windows-native: rewrite POSIX-mount paths (WSL `/mnt/<x>/`,
* Cygwin `/cygdrive/<x>/`, MSYS `/<x>/`) to drive form `<x>:/...`.
* The single-letter + separator check below is what stops
* multi-character segments like `/mnt/storage` from matching.
*/
static const struct {
const char *prefix;
size_t prefix_len;
} posix_prefixes[] = {
{ "/mnt/", 5 }, /* WSL */
{ "/cygdrive/", 10 }, /* Cygwin */
{ "/", 1 }, /* MSYS */
};
size_t i;

if (path->len == 0)
return 0;

for (i = 0; i < ARRAY_SIZE(posix_prefixes); i++) {
size_t pl = posix_prefixes[i].prefix_len;
char drive;

if (path->len < pl + 1)
continue;
if (memcmp(path->buf, posix_prefixes[i].prefix, pl) != 0)
continue;
drive = path->buf[pl];
if (!isalpha((unsigned char)drive))
continue;
if (path->len > pl + 1 && path->buf[pl + 1] != '/' && path->buf[pl + 1] != '\\')
continue;

/* "<prefix><drive>" (pl+1 bytes) -> "<drive>:" (2 bytes). */
path->buf[0] = drive;
path->buf[1] = ':';
memmove(path->buf + 2, path->buf + pl + 1, path->len - pl);
strbuf_setlen(path, path->len - (pl - 1));
return 1;
}
return 0;
#else
/*
* POSIX: rewrite Windows-form `<x>:/...` or `<x>:\...` to this
* build's mount form (drive_prefix selected below), normalising
* any backslashes in the tail.
*/
#if defined(__MSYS__)
static const char drive_prefix[] = "/";
#elif defined(__CYGWIN__)
static const char drive_prefix[] = "/cygdrive/";
#else
static const char drive_prefix[] = "/mnt/";
#endif
const size_t drive_prefix_len = sizeof(drive_prefix) - 1;
const size_t expansion = drive_prefix_len + 1 - 2;
char drive;
size_t i;

if (path->len < 3)
return 0;
if (!isalpha((unsigned char)path->buf[0]))
return 0;
if (path->buf[1] != ':')
return 0;
if (path->buf[2] != '/' && path->buf[2] != '\\')
return 0;

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

strbuf_grow(path, expansion);
memmove(path->buf + 2 + expansion, path->buf + 2, path->len - 2 + 1);
memcpy(path->buf, drive_prefix, drive_prefix_len);
path->buf[drive_prefix_len] = drive;
path->len += expansion;

for (i = drive_prefix_len + 1; i < path->len; i++) {
if (path->buf[i] == '\\')
path->buf[i] = '/';
}
return 1;
#endif
}

static int get_st_mode_bits(const char *path, int *mode)
{
struct stat st;
Expand Down
19 changes: 19 additions & 0 deletions path.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ struct strbuf;
struct string_list;
struct worktree;

/*
* Translate worktree gitdir paths between native Windows and POSIX-mount
* forms, in the direction appropriate for the current build:
*
* * Windows-native: `/mnt/<x>/`, `/cygdrive/<x>/`, or `/<x>/` -> `<x>:/...`
* * MSYS: `<x>:/` or `<x>:\` -> `/<x>/...`
* * Cygwin: `<x>:/` or `<x>:\` -> `/cygdrive/<x>/...`
* * everything else (Linux/WSL/macOS/...): `<x>:/` or `<x>:\` -> `/mnt/<x>/...`
*
* `<x>` must be a single ASCII letter; multi-character segments
* (`/mnt/storage`) and digit-prefixed mounts pass through unchanged.
* Backslashes in the tail are normalised to forward slashes on the
* POSIX-direction translation.
*
* Edits `path` in place; may shrink (Windows direction) or grow (POSIX
* direction). Returns 1 if a translation occurred, 0 otherwise.
*/
int translate_windows_path(struct strbuf *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
Expand Down
14 changes: 14 additions & 0 deletions setup.c
Original file line number Diff line number Diff line change
Expand Up @@ -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_windows_path(&data);
if (!is_absolute_path(data.buf))
strbuf_addf(&path, "%s/", gitdir);
strbuf_addbuf(&path, &data);
Expand Down Expand Up @@ -1009,6 +1010,19 @@ const char *read_gitfile_gently(const char *path, int *return_error_code)
buf[len] = '\0';
dir = buf + 8;

{
struct strbuf translated = STRBUF_INIT;
strbuf_addstr(&translated, dir);
if (translate_windows_path(&translated)) {
char *new_buf = xstrfmt("gitdir: %s", translated.buf);
free(buf);
buf = new_buf;
len = strlen(buf);
dir = buf + 8;
}
strbuf_release(&translated);
}

if (!is_absolute_path(dir) && (slash = strrchr(path, '/'))) {
size_t pathlen = slash+1 - path;
dir = xstrfmt("%.*s%.*s", (int)pathlen, path,
Expand Down
9 changes: 9 additions & 0 deletions t/helper/test-path-utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,15 @@ int cmd__path_utils(int argc, const char **argv)
return 0;
}

if (argc == 3 && !strcmp(argv[1], "translate_windows_path")) {
struct strbuf sb = STRBUF_INIT;
strbuf_addstr(&sb, argv[2]);
translate_windows_path(&sb);
puts(sb.buf);
strbuf_release(&sb);
return 0;
}

if (argc == 4 && !strcmp(argv[1], "relative_path")) {
struct strbuf sb = STRBUF_INIT;
const char *in, *prefix, *rel;
Expand Down
149 changes: 149 additions & 0 deletions t/t0042-wsl-mnt-path.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/bin/sh

test_description='translate WSL/Cygwin /mnt/<x>/ 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 a POSIX-mount
# form rooted at $1. Handles MSYS form (/c/foo), Windows forward-slash
# form (C:/foo), and Windows backslash form (C:\foo). $1 may be empty,
# in which case the result is the MSYS2 mount form (/c/foo). MINGW-only.
mount_form () {
prefix=$1
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"; }
to_msys () { mount_form "" "$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/<x>/ 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/<x>/ 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 translates /<x>/ MSYS2 gitdir' '
test_when_finished "rm -rf wtlink actual" &&
REAL=$(cd repo/.git && pwd) &&
MSYS_PATH=$(to_msys "$REAL") &&

case "$MSYS_PATH" in
/[A-Za-z]/*) : ok ;;
*) BUG "to_msys produced $MSYS_PATH from $REAL" ;;
esac &&

mkdir wtlink &&
printf "gitdir: %s\n" "$MSYS_PATH" >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/<multichar>/ 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/<x>/ 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/<x>/ 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/<x>/ 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
28 changes: 28 additions & 0 deletions t/t0060-path-utils.sh
Original file line number Diff line number Diff line change
Expand Up @@ -611,4 +611,32 @@ test_expect_success !VALGRIND,RUNTIME_PREFIX,CAN_EXEC_IN_PWD '%(prefix)/ works'
test_cmp expect actual
'

case "$(uname -s)" in
*CYGWIN*) MOUNT_PREFIX=/cygdrive ;;
*MSYS_NT*) MOUNT_PREFIX= ;;
*) MOUNT_PREFIX=/mnt ;;
esac

translate_windows_path() {
test_expect_success !MINGW "translate_windows_path: $1 => $2" "
echo '$2' >expect &&
test-tool path-utils translate_windows_path '$1' >actual &&
test_cmp expect actual
"
}

translate_windows_path 'C:/foo/bar' "$MOUNT_PREFIX/c/foo/bar"
translate_windows_path 'C:\foo\bar' "$MOUNT_PREFIX/c/foo/bar"
translate_windows_path 'D:/repo/.git/worktrees/wt' "$MOUNT_PREFIX/d/repo/.git/worktrees/wt"
translate_windows_path 'Z:\path\with mixed/seps' "$MOUNT_PREFIX/z/path/with mixed/seps"
translate_windows_path 'c:/already-lower' "$MOUNT_PREFIX/c/already-lower"

# Inputs that must NOT be translated:
translate_windows_path '/already/posix' '/already/posix'
translate_windows_path 'relative/path' 'relative/path'
translate_windows_path 'C:relative-no-separator' 'C:relative-no-separator'
translate_windows_path '1:/digit-prefix' '1:/digit-prefix'
translate_windows_path 'C' 'C'
translate_windows_path '' ''

test_done
15 changes: 15 additions & 0 deletions worktree.c
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ struct worktree *get_linked_worktree(const char *id,
strbuf_rtrim(&worktree_path);
strbuf_strip_suffix(&worktree_path, "/.git");

/* Convert Windows path to POSIX path or vice-versa. */
translate_windows_path(&worktree_path);

if (!is_absolute_path(worktree_path.buf)) {
strbuf_strip_suffix(&path, "gitdir");
strbuf_addbuf(&path, &worktree_path);
Expand Down Expand Up @@ -992,6 +995,18 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath,
goto done;
}
path[len] = '\0';

{
struct strbuf translated = STRBUF_INIT;
strbuf_addstr(&translated, path);
if (translate_windows_path(&translated)) {
free(path);
path = strbuf_detach(&translated, NULL);
} else {
strbuf_release(&translated);
}
}

if (is_absolute_path(path)) {
strbuf_addstr(&dotgit, path);
} else {
Expand Down
Loading