Skip to content

Commit

Permalink
Merge pull request git-for-windows#1897 from piscisaureus/symlink-attr
Browse files Browse the repository at this point in the history
Specify symlink type in .gitattributes
  • Loading branch information
dscho committed Aug 29, 2024
2 parents a4b9654 + c97d837 commit 0ad75d5
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 48 deletions.
30 changes: 30 additions & 0 deletions Documentation/gitattributes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,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`
^^^^^^^^

Expand Down
2 changes: 1 addition & 1 deletion apply.c
Original file line number Diff line number Diff line change
Expand Up @@ -4440,7 +4440,7 @@ static int try_create_file(struct apply_state *state, const char *path,
/* Although buf:size is counted string, it also is NUL
* terminated.
*/
return !!symlink(buf, path);
return !!create_symlink(state && state->repo ? state->repo->index : NULL, buf, path);

fd = open(path, O_CREAT | O_EXCL | O_WRONLY, (mode & 0100) ? 0777 : 0666);
if (fd < 0)
Expand Down
2 changes: 1 addition & 1 deletion builtin/difftool.c
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ static int run_dir_diff(const char *extcmd, int symlinks, const char *prefix,
}
add_path(&wtdir, wtdir_len, dst_path);
if (symlinks) {
if (symlink(wtdir.buf, rdir.buf)) {
if (create_symlink(lstate.istate, wtdir.buf, rdir.buf)) {
ret = error_errno("could not symlink '%s' to '%s'", wtdir.buf, rdir.buf);
goto finish;
}
Expand Down
143 changes: 103 additions & 40 deletions compat/mingw.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "gettext.h"
#define SECURITY_WIN32
#include <sspi.h>
#include "../attr.h"

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

Expand Down Expand Up @@ -427,6 +428,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_PATH];
struct phantom_symlink_info *psi;

/* convert to absolute path to be independent of cwd */
len = GetFullPathNameW(wlink, MAX_PATH, wfullpath, NULL);
if (!len || len >= MAX_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)
{
Expand Down Expand Up @@ -2618,7 +2667,38 @@ int link(const char *oldpath, const char *newpath)
return 0;
}

int symlink(const char *target, const char *link)
enum symlink_type {
SYMLINK_TYPE_UNSPECIFIED = 0,
SYMLINK_TYPE_FILE,
SYMLINK_TYPE_DIRECTORY,
};

static enum symlink_type check_symlink_attr(struct index_state *index, const char *link)
{
static struct attr_check *check;
const char *value;

if (!index)
return SYMLINK_TYPE_UNSPECIFIED;

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

git_check_attr(index, link, check);

value = check->items[0].value;
if (ATTR_UNSET(value))
return SYMLINK_TYPE_UNSPECIFIED;
if (!strcmp(value, "file"))
return SYMLINK_TYPE_FILE;
if (!strcmp(value, "dir") || !strcmp(value, "directory"))
return SYMLINK_TYPE_DIRECTORY;

warning(_("ignoring invalid symlink type '%s' for '%s'"), value, link);
return SYMLINK_TYPE_UNSPECIFIED;
}

int mingw_create_symlink(struct index_state *index, const char *target, const char *link)
{
wchar_t wtarget[MAX_PATH], wlink[MAX_PATH];
int len;
Expand All @@ -2638,48 +2718,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_PATH];
struct phantom_symlink_info *psi;

/* convert to absolute path to be independent of cwd */
len = GetFullPathNameW(wlink, MAX_PATH, wfullpath, NULL);
if (!len || len >= MAX_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(index, 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
Expand Down
4 changes: 3 additions & 1 deletion compat/mingw.h
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,10 @@ int setitimer(int type, struct itimerval *in, struct itimerval *out);
int sigaction(int sig, struct sigaction *in, struct sigaction *out);
int link(const char *oldpath, const char *newpath);
int uname(struct utsname *buf);
int symlink(const char *target, const char *link);
int readlink(const char *path, char *buf, size_t bufsiz);
struct index_state;
int mingw_create_symlink(struct index_state *index, const char *target, const char *link);
#define create_symlink mingw_create_symlink

/*
* replacements of existing functions
Expand Down
2 changes: 1 addition & 1 deletion entry.c
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ static int write_entry(struct cache_entry *ce, char *path, struct conv_attrs *ca
if (!has_symlinks || to_tempfile)
goto write_file_entry;

ret = symlink(new_blob, path);
ret = create_symlink(state->istate, new_blob, path);
free(new_blob);
if (ret)
return error_errno("unable to create symlink %s", path);
Expand Down
9 changes: 9 additions & 0 deletions git-compat-util.h
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,15 @@ static inline int git_has_dir_sep(const char *path)
#define has_dir_sep(path) git_has_dir_sep(path)
#endif

#ifndef create_symlink
struct index_state;
static inline int git_create_symlink(struct index_state *index, const char *target, const char *link)
{
return symlink(target, link);
}
#define create_symlink git_create_symlink
#endif

#ifndef query_user_email
#define query_user_email() NULL
#endif
Expand Down
2 changes: 1 addition & 1 deletion merge-recursive.c
Original file line number Diff line number Diff line change
Expand Up @@ -1004,7 +1004,7 @@ static int update_file_flags(struct merge_options *opt,
char *lnk = xmemdupz(buf, size);
safe_create_leading_directories_const(path);
unlink(path);
if (symlink(lnk, path))
if (create_symlink(&opt->priv->orig_index, lnk, path))
ret = err(opt, _("failed to symlink '%s': %s"),
path, strerror(errno));
free(lnk);
Expand Down
2 changes: 1 addition & 1 deletion refs/files-backend.c
Original file line number Diff line number Diff line change
Expand Up @@ -1952,7 +1952,7 @@ static int create_ref_symlink(struct ref_lock *lock, const char *target)
#ifndef NO_SYMLINK_HEAD
char *ref_path = get_locked_file_path(&lock->lk);
unlink(ref_path);
ret = symlink(target, ref_path);
ret = create_symlink(NULL, target, ref_path);
free(ref_path);

if (ret)
Expand Down
4 changes: 2 additions & 2 deletions setup.c
Original file line number Diff line number Diff line change
Expand Up @@ -2012,7 +2012,7 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
if (strbuf_readlink(&lnk, template_path->buf,
st_template.st_size) < 0)
die_errno(_("cannot readlink '%s'"), template_path->buf);
if (symlink(lnk.buf, path->buf))
if (create_symlink(NULL, lnk.buf, path->buf))
die_errno(_("cannot symlink '%s' '%s'"),
lnk.buf, path->buf);
strbuf_release(&lnk);
Expand Down Expand Up @@ -2259,7 +2259,7 @@ static int create_default_files(const char *template_path,
path = git_path_buf(&buf, "tXXXXXX");
if (!close(xmkstemp(path)) &&
!unlink(path) &&
!symlink("testing", path) &&
!create_symlink(NULL, "testing", path) &&
!lstat(path, &st1) &&
S_ISLNK(st1.st_mode))
unlink(path); /* good */
Expand Down
46 changes: 46 additions & 0 deletions t/t2040-checkout-symlink-attr.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/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"
}

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 "[a]b=c" >file1 &&
echo "[x]y=z" >dir/file2 &&
# MSYS2 is very forgiving, it will resolve symlinks even if the
# symlink type is incorrect. To make this test meaningful, try
# them with a native, non-MSYS executable, such as `git config`.
test "$(git config -f file-link a.b)" = "c" &&
test "$(git config -f dir-link/file2 x.y)" = "z"
'

test_done

0 comments on commit 0ad75d5

Please sign in to comment.