Skip to content
Permalink
Browse files

Merge pull request #1897 from piscisaureus/symlink-attr

Specify symlink type in .gitattributes
  • Loading branch information...
dscho committed Oct 31, 2018
2 parents d4f95fc + 1a7fea1 commit 25a7f44187bb9d208ec34401b5b25db4641127d6
Showing with 181 additions and 39 deletions.
  1. +30 −0 Documentation/gitattributes.txt
  2. +100 −39 compat/mingw.c
  3. +51 −0 t/t2040-checkout-symlink-attr.sh
@@ -378,6 +378,36 @@ sign `$` upon checkout. Any byte sequence that begins with
with `$Id$` upon check-in.


`symlink`
^^^^^^^^^

On Windows, symbolic links have a type: a "file symlink" must point at
a file, and a "directory symlink" must point at a directory. If the
type of symlink does not match its target, it doesn't work.

Git does not record the type of symlink in the index or in a tree. On
checkout it'll guess the type, which only works if the target exists
at the time the symlink is created. This may often not be the case,
for example when the link points at a directory inside a submodule.

The `symlink` attribute allows you to explicitly set the type of symlink
to `file` or `dir`, so Git doesn't have to guess. If you have a set of
symlinks that point at other files, you can do:

------------------------
*.gif symlink=file
------------------------

To tell Git that a symlink points at a directory, use:

------------------------
tools_folder symlink=dir
------------------------

The `symlink` attribute is ignored on platforms other than Windows,
since they don't distinguish between different types of symlinks.


`filter`
^^^^^^^^

@@ -10,6 +10,7 @@
#include "win32/lazyload.h"
#include "../config.h"
#include "../string-list.h"
#include "../attr.h"

#define HCAST(type, handle) ((type)(intptr_t)handle)

@@ -401,6 +402,54 @@ static void process_phantom_symlinks(void)
LeaveCriticalSection(&phantom_symlinks_cs);
}

static int create_phantom_symlink(wchar_t *wtarget, wchar_t *wlink)
{
int len;

/* create file symlink */
if (!CreateSymbolicLinkW(wlink, wtarget, symlink_file_flags)) {
errno = err_win_to_posix(GetLastError());
return -1;
}

/* convert to directory symlink if target exists */
switch (process_phantom_symlink(wtarget, wlink)) {
case PHANTOM_SYMLINK_RETRY: {
/* if target doesn't exist, add to phantom symlinks list */
wchar_t wfullpath[MAX_LONG_PATH];
struct phantom_symlink_info *psi;

/* convert to absolute path to be independent of cwd */
len = GetFullPathNameW(wlink, MAX_LONG_PATH, wfullpath, NULL);
if (!len || len >= MAX_LONG_PATH) {
errno = err_win_to_posix(GetLastError());
return -1;
}

/* over-allocate and fill phantom_symlink_info structure */
psi = xmalloc(sizeof(struct phantom_symlink_info) +
sizeof(wchar_t) * (len + wcslen(wtarget) + 2));
psi->wlink = (wchar_t *)(psi + 1);
wcscpy(psi->wlink, wfullpath);
psi->wtarget = psi->wlink + len + 1;
wcscpy(psi->wtarget, wtarget);

EnterCriticalSection(&phantom_symlinks_cs);
psi->next = phantom_symlinks;
phantom_symlinks = psi;
LeaveCriticalSection(&phantom_symlinks_cs);
break;
}
case PHANTOM_SYMLINK_DIRECTORY:
/* if we created a dir symlink, process other phantom symlinks */
process_phantom_symlinks();
break;
default:
break;
}
return 0;
}

/* Normalizes NT paths as returned by some low-level APIs. */
static wchar_t *normalize_ntpath(wchar_t *wbuf)
{
@@ -2819,6 +2868,35 @@ int link(const char *oldpath, const char *newpath)
return 0;
}

enum symlink_type {
SYMLINK_TYPE_UNSPECIFIED = 0,
SYMLINK_TYPE_FILE,
SYMLINK_TYPE_DIRECTORY,
};

static enum symlink_type check_symlink_attr(const char *link)
{
static struct attr_check *check;
const char *value;
int r;

if (!check)
check = attr_check_initl("symlink", NULL);

r = git_check_attr(&the_index, link, check);
assert(!r);

value = check->items[0].value;
if (value == NULL)
;
else if (!strcmp(value, "file"))
return SYMLINK_TYPE_FILE;
else if (!strcmp(value, "dir"))
return SYMLINK_TYPE_DIRECTORY;

return SYMLINK_TYPE_UNSPECIFIED;
}

int symlink(const char *target, const char *link)
{
wchar_t wtarget[MAX_LONG_PATH], wlink[MAX_LONG_PATH];
@@ -2839,48 +2917,31 @@ int symlink(const char *target, const char *link)
if (wtarget[len] == '/')
wtarget[len] = '\\';

/* create file symlink */
if (!CreateSymbolicLinkW(wlink, wtarget, symlink_file_flags)) {
errno = err_win_to_posix(GetLastError());
return -1;
}

/* convert to directory symlink if target exists */
switch (process_phantom_symlink(wtarget, wlink)) {
case PHANTOM_SYMLINK_RETRY: {
/* if target doesn't exist, add to phantom symlinks list */
wchar_t wfullpath[MAX_LONG_PATH];
struct phantom_symlink_info *psi;

/* convert to absolute path to be independent of cwd */
len = GetFullPathNameW(wlink, MAX_LONG_PATH, wfullpath, NULL);
if (!len || len >= MAX_LONG_PATH) {
errno = err_win_to_posix(GetLastError());
return -1;
}

/* over-allocate and fill phantom_symlink_info structure */
psi = xmalloc(sizeof(struct phantom_symlink_info)
+ sizeof(wchar_t) * (len + wcslen(wtarget) + 2));
psi->wlink = (wchar_t *)(psi + 1);
wcscpy(psi->wlink, wfullpath);
psi->wtarget = psi->wlink + len + 1;
wcscpy(psi->wtarget, wtarget);

EnterCriticalSection(&phantom_symlinks_cs);
psi->next = phantom_symlinks;
phantom_symlinks = psi;
LeaveCriticalSection(&phantom_symlinks_cs);
break;
}
case PHANTOM_SYMLINK_DIRECTORY:
/* if we created a dir symlink, process other phantom symlinks */
switch (check_symlink_attr(link)) {
case SYMLINK_TYPE_UNSPECIFIED:
/* Create a phantom symlink: it is initially created as a file
* symlink, but may change to a directory symlink later if/when
* the target exists. */
return create_phantom_symlink(wtarget, wlink);
case SYMLINK_TYPE_FILE:
if (!CreateSymbolicLinkW(wlink, wtarget, symlink_file_flags))
break;
return 0;
case SYMLINK_TYPE_DIRECTORY:
if (!CreateSymbolicLinkW(wlink, wtarget,
symlink_directory_flags))
break;
/* There may be dangling phantom symlinks that point at this
* one, which should now morph into directory symlinks. */
process_phantom_symlinks();
break;
return 0;
default:
break;
BUG("unhandled symlink type");
}
return 0;

/* CreateSymbolicLinkW failed. */
errno = err_win_to_posix(GetLastError());
return -1;
}

#ifndef _WINNT_H
@@ -0,0 +1,51 @@
#!/bin/sh

test_description='checkout symlinks with `symlink` attribute on Windows
Ensures that Git for Windows creates symlinks of the right type,
as specified by the `symlink` attribute in `.gitattributes`.'

# Tell MSYS to create native symlinks. Without this flag test-lib's
# prerequisite detection for SYMLINKS doesn't detect the right thing.
MSYS=winsymlinks:nativestrict && export MSYS

. ./test-lib.sh

if ! test_have_prereq MINGW,SYMLINKS
then
skip_all='skipping $0: MinGW-only test, which requires symlink support.'
test_done
fi

# Adds a symlink to the index without clobbering the work tree.
cache_symlink () {
sha=$(printf '%s' "$1" | git hash-object --stdin -w) &&
git update-index --add --cacheinfo 120000,$sha,"$2"
}

# MSYS2 is very forgiving, it will resolve symlinks even if the
# symlink type isn't correct. To make this test meaningful, try
# them with a native, non-MSYS executable.
cat_native () {
filename=$(cygpath -w "$1") &&
cmd.exe /c "type \"$filename\""
}

test_expect_success 'checkout symlinks with attr' '
cache_symlink file1 file-link &&
cache_symlink dir dir-link &&
printf "file-link symlink=file\ndir-link symlink=dir\n" >.gitattributes &&
git add .gitattributes &&
git checkout . &&
mkdir dir &&
echo "contents1" >file1 &&
echo "contents2" >dir/file2 &&
test "$(cat_native file-link)" = "contents1" &&
test "$(cat_native dir-link/file2)" = "contents2"
'

test_done

0 comments on commit 25a7f44

Please sign in to comment.
You can’t perform that action at this time.