Skip to content

Commit 5725713

Browse files
dschoGit for Windows Build Agent
authored andcommitted
Merge pull request #1897 from piscisaureus/symlink-attr
Specify symlink type in .gitattributes
2 parents c1fd121 + 3528052 commit 5725713

File tree

11 files changed

+199
-47
lines changed

11 files changed

+199
-47
lines changed

Documentation/gitattributes.adoc

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,36 @@ sign `$` upon checkout. Any byte sequence that begins with
403403
with `$Id$` upon check-in.
404404

405405

406+
`symlink`
407+
^^^^^^^^^
408+
409+
On Windows, symbolic links have a type: a "file symlink" must point at
410+
a file, and a "directory symlink" must point at a directory. If the
411+
type of symlink does not match its target, it doesn't work.
412+
413+
Git does not record the type of symlink in the index or in a tree. On
414+
checkout it'll guess the type, which only works if the target exists
415+
at the time the symlink is created. This may often not be the case,
416+
for example when the link points at a directory inside a submodule.
417+
418+
The `symlink` attribute allows you to explicitly set the type of symlink
419+
to `file` or `dir`, so Git doesn't have to guess. If you have a set of
420+
symlinks that point at other files, you can do:
421+
422+
------------------------
423+
*.gif symlink=file
424+
------------------------
425+
426+
To tell Git that a symlink points at a directory, use:
427+
428+
------------------------
429+
tools_folder symlink=dir
430+
------------------------
431+
432+
The `symlink` attribute is ignored on platforms other than Windows,
433+
since they don't distinguish between different types of symlinks.
434+
435+
406436
`filter`
407437
^^^^^^^^
408438

apply.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4398,7 +4398,7 @@ static int try_create_file(struct apply_state *state, const char *path,
43984398
/* Although buf:size is counted string, it also is NUL
43994399
* terminated.
44004400
*/
4401-
return !!symlink(buf, path);
4401+
return !!create_symlink(state && state->repo ? state->repo->index : NULL, buf, path);
44024402

44034403
fd = open(path, O_CREAT | O_EXCL | O_WRONLY, (mode & 0100) ? 0777 : 0666);
44044404
if (fd < 0)

builtin/difftool.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ static int run_dir_diff(struct repository *repo,
544544
}
545545
add_path(&wtdir, wtdir_len, dst_path);
546546
if (dt_options->symlinks) {
547-
if (symlink(wtdir.buf, rdir.buf)) {
547+
if (create_symlink(lstate.istate, wtdir.buf, rdir.buf)) {
548548
ret = error_errno("could not symlink '%s' to '%s'", wtdir.buf, rdir.buf);
549549
goto finish;
550550
}

compat/mingw-posix.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,10 @@ int setitimer(int type, struct itimerval *in, struct itimerval *out);
192192
int sigaction(int sig, struct sigaction *in, struct sigaction *out);
193193
int link(const char *oldpath, const char *newpath);
194194
int uname(struct utsname *buf);
195-
int symlink(const char *target, const char *link);
196195
int readlink(const char *path, char *buf, size_t bufsiz);
196+
struct index_state;
197+
int mingw_create_symlink(struct index_state *index, const char *target, const char *link);
198+
#define create_symlink mingw_create_symlink
197199

198200
/*
199201
* replacements of existing functions

compat/mingw.c

Lines changed: 103 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "git-compat-util.h"
55
#include "abspath.h"
66
#include "alloc.h"
7+
#include "attr.h"
78
#include "config.h"
89
#include "dir.h"
910
#include "environment.h"
@@ -454,6 +455,54 @@ static void process_phantom_symlinks(void)
454455
LeaveCriticalSection(&phantom_symlinks_cs);
455456
}
456457

458+
static int create_phantom_symlink(wchar_t *wtarget, wchar_t *wlink)
459+
{
460+
int len;
461+
462+
/* create file symlink */
463+
if (!CreateSymbolicLinkW(wlink, wtarget, symlink_file_flags)) {
464+
errno = err_win_to_posix(GetLastError());
465+
return -1;
466+
}
467+
468+
/* convert to directory symlink if target exists */
469+
switch (process_phantom_symlink(wtarget, wlink)) {
470+
case PHANTOM_SYMLINK_RETRY: {
471+
/* if target doesn't exist, add to phantom symlinks list */
472+
wchar_t wfullpath[MAX_LONG_PATH];
473+
struct phantom_symlink_info *psi;
474+
475+
/* convert to absolute path to be independent of cwd */
476+
len = GetFullPathNameW(wlink, MAX_LONG_PATH, wfullpath, NULL);
477+
if (!len || len >= MAX_LONG_PATH) {
478+
errno = err_win_to_posix(GetLastError());
479+
return -1;
480+
}
481+
482+
/* over-allocate and fill phantom_symlink_info structure */
483+
psi = xmalloc(sizeof(struct phantom_symlink_info) +
484+
sizeof(wchar_t) * (len + wcslen(wtarget) + 2));
485+
psi->wlink = (wchar_t *)(psi + 1);
486+
wcscpy(psi->wlink, wfullpath);
487+
psi->wtarget = psi->wlink + len + 1;
488+
wcscpy(psi->wtarget, wtarget);
489+
490+
EnterCriticalSection(&phantom_symlinks_cs);
491+
psi->next = phantom_symlinks;
492+
phantom_symlinks = psi;
493+
LeaveCriticalSection(&phantom_symlinks_cs);
494+
break;
495+
}
496+
case PHANTOM_SYMLINK_DIRECTORY:
497+
/* if we created a dir symlink, process other phantom symlinks */
498+
process_phantom_symlinks();
499+
break;
500+
default:
501+
break;
502+
}
503+
return 0;
504+
}
505+
457506
/* Normalizes NT paths as returned by some low-level APIs. */
458507
static wchar_t *normalize_ntpath(wchar_t *wbuf)
459508
{
@@ -3124,7 +3173,38 @@ int link(const char *oldpath, const char *newpath)
31243173
return 0;
31253174
}
31263175

3127-
int symlink(const char *target, const char *link)
3176+
enum symlink_type {
3177+
SYMLINK_TYPE_UNSPECIFIED = 0,
3178+
SYMLINK_TYPE_FILE,
3179+
SYMLINK_TYPE_DIRECTORY,
3180+
};
3181+
3182+
static enum symlink_type check_symlink_attr(struct index_state *index, const char *link)
3183+
{
3184+
static struct attr_check *check;
3185+
const char *value;
3186+
3187+
if (!index)
3188+
return SYMLINK_TYPE_UNSPECIFIED;
3189+
3190+
if (!check)
3191+
check = attr_check_initl("symlink", NULL);
3192+
3193+
git_check_attr(index, link, check);
3194+
3195+
value = check->items[0].value;
3196+
if (ATTR_UNSET(value))
3197+
return SYMLINK_TYPE_UNSPECIFIED;
3198+
if (!strcmp(value, "file"))
3199+
return SYMLINK_TYPE_FILE;
3200+
if (!strcmp(value, "dir") || !strcmp(value, "directory"))
3201+
return SYMLINK_TYPE_DIRECTORY;
3202+
3203+
warning(_("ignoring invalid symlink type '%s' for '%s'"), value, link);
3204+
return SYMLINK_TYPE_UNSPECIFIED;
3205+
}
3206+
3207+
int mingw_create_symlink(struct index_state *index, const char *target, const char *link)
31283208
{
31293209
wchar_t wtarget[MAX_LONG_PATH], wlink[MAX_LONG_PATH];
31303210
int len;
@@ -3144,48 +3224,31 @@ int symlink(const char *target, const char *link)
31443224
if (wtarget[len] == '/')
31453225
wtarget[len] = '\\';
31463226

3147-
/* create file symlink */
3148-
if (!CreateSymbolicLinkW(wlink, wtarget, symlink_file_flags)) {
3149-
errno = err_win_to_posix(GetLastError());
3150-
return -1;
3151-
}
3152-
3153-
/* convert to directory symlink if target exists */
3154-
switch (process_phantom_symlink(wtarget, wlink)) {
3155-
case PHANTOM_SYMLINK_RETRY: {
3156-
/* if target doesn't exist, add to phantom symlinks list */
3157-
wchar_t wfullpath[MAX_LONG_PATH];
3158-
struct phantom_symlink_info *psi;
3159-
3160-
/* convert to absolute path to be independent of cwd */
3161-
len = GetFullPathNameW(wlink, MAX_LONG_PATH, wfullpath, NULL);
3162-
if (!len || len >= MAX_LONG_PATH) {
3163-
errno = err_win_to_posix(GetLastError());
3164-
return -1;
3165-
}
3166-
3167-
/* over-allocate and fill phantom_symlink_info structure */
3168-
psi = xmalloc(sizeof(struct phantom_symlink_info)
3169-
+ sizeof(wchar_t) * (len + wcslen(wtarget) + 2));
3170-
psi->wlink = (wchar_t *)(psi + 1);
3171-
wcscpy(psi->wlink, wfullpath);
3172-
psi->wtarget = psi->wlink + len + 1;
3173-
wcscpy(psi->wtarget, wtarget);
3174-
3175-
EnterCriticalSection(&phantom_symlinks_cs);
3176-
psi->next = phantom_symlinks;
3177-
phantom_symlinks = psi;
3178-
LeaveCriticalSection(&phantom_symlinks_cs);
3179-
break;
3180-
}
3181-
case PHANTOM_SYMLINK_DIRECTORY:
3182-
/* if we created a dir symlink, process other phantom symlinks */
3227+
switch (check_symlink_attr(index, link)) {
3228+
case SYMLINK_TYPE_UNSPECIFIED:
3229+
/* Create a phantom symlink: it is initially created as a file
3230+
* symlink, but may change to a directory symlink later if/when
3231+
* the target exists. */
3232+
return create_phantom_symlink(wtarget, wlink);
3233+
case SYMLINK_TYPE_FILE:
3234+
if (!CreateSymbolicLinkW(wlink, wtarget, symlink_file_flags))
3235+
break;
3236+
return 0;
3237+
case SYMLINK_TYPE_DIRECTORY:
3238+
if (!CreateSymbolicLinkW(wlink, wtarget,
3239+
symlink_directory_flags))
3240+
break;
3241+
/* There may be dangling phantom symlinks that point at this
3242+
* one, which should now morph into directory symlinks. */
31833243
process_phantom_symlinks();
3184-
break;
3244+
return 0;
31853245
default:
3186-
break;
3246+
BUG("unhandled symlink type");
31873247
}
3188-
return 0;
3248+
3249+
/* CreateSymbolicLinkW failed. */
3250+
errno = err_win_to_posix(GetLastError());
3251+
return -1;
31893252
}
31903253

31913254
#ifndef _WINNT_H

entry.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ static int write_entry(struct cache_entry *ce, char *path, struct conv_attrs *ca
324324
if (!has_symlinks || to_tempfile)
325325
goto write_file_entry;
326326

327-
ret = symlink(new_blob, path);
327+
ret = create_symlink(state->istate, new_blob, path);
328328
free(new_blob);
329329
if (ret)
330330
return error_errno("unable to create symlink %s", path);

git-compat-util.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,16 @@ static inline int git_has_dir_sep(const char *path)
396396
#define is_mount_point is_mount_point_via_stat
397397
#endif
398398

399+
#ifndef create_symlink
400+
struct index_state;
401+
static inline int git_create_symlink(struct index_state *index UNUSED,
402+
const char *target, const char *link)
403+
{
404+
return symlink(target, link);
405+
}
406+
#define create_symlink git_create_symlink
407+
#endif
408+
399409
#ifndef query_user_email
400410
#define query_user_email() NULL
401411
#endif

refs/files-backend.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2053,7 +2053,7 @@ static int create_ref_symlink(struct ref_lock *lock, const char *target)
20532053

20542054
char *ref_path = get_locked_file_path(&lock->lk);
20552055
unlink(ref_path);
2056-
ret = symlink(target, ref_path);
2056+
ret = create_symlink(NULL, target, ref_path);
20572057
free(ref_path);
20582058

20592059
if (ret)

setup.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2135,7 +2135,7 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
21352135
if (strbuf_readlink(&lnk, template_path->buf,
21362136
st_template.st_size) < 0)
21372137
die_errno(_("cannot readlink '%s'"), template_path->buf);
2138-
if (symlink(lnk.buf, path->buf))
2138+
if (create_symlink(NULL, lnk.buf, path->buf))
21392139
die_errno(_("cannot symlink '%s' '%s'"),
21402140
lnk.buf, path->buf);
21412141
strbuf_release(&lnk);
@@ -2396,7 +2396,7 @@ static int create_default_files(const char *template_path,
23962396
repo_git_path_replace(the_repository, &path, "tXXXXXX");
23972397
if (!close(xmkstemp(path.buf)) &&
23982398
!unlink(path.buf) &&
2399-
!symlink("testing", path.buf) &&
2399+
!create_symlink(NULL, "testing", path.buf) &&
24002400
!lstat(path.buf, &st1) &&
24012401
S_ISLNK(st1.st_mode))
24022402
unlink(path.buf); /* good */

t/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ integration_tests = [
257257
't2027-checkout-track.sh',
258258
't2030-unresolve-info.sh',
259259
't2031-checkout-long-paths.sh',
260+
't2040-checkout-symlink-attr.sh',
260261
't2050-git-dir-relative.sh',
261262
't2060-switch.sh',
262263
't2070-restore.sh',

t/t2040-checkout-symlink-attr.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/bin/sh
2+
3+
test_description='checkout symlinks with `symlink` attribute on Windows
4+
5+
Ensures that Git for Windows creates symlinks of the right type,
6+
as specified by the `symlink` attribute in `.gitattributes`.'
7+
8+
# Tell MSYS to create native symlinks. Without this flag test-lib's
9+
# prerequisite detection for SYMLINKS doesn't detect the right thing.
10+
MSYS=winsymlinks:nativestrict && export MSYS
11+
12+
. ./test-lib.sh
13+
14+
if ! test_have_prereq MINGW,SYMLINKS
15+
then
16+
skip_all='skipping $0: MinGW-only test, which requires symlink support.'
17+
test_done
18+
fi
19+
20+
# Adds a symlink to the index without clobbering the work tree.
21+
cache_symlink () {
22+
sha=$(printf '%s' "$1" | git hash-object --stdin -w) &&
23+
git update-index --add --cacheinfo 120000,$sha,"$2"
24+
}
25+
26+
test_expect_success 'checkout symlinks with attr' '
27+
cache_symlink file1 file-link &&
28+
cache_symlink dir dir-link &&
29+
30+
printf "file-link symlink=file\ndir-link symlink=dir\n" >.gitattributes &&
31+
git add .gitattributes &&
32+
33+
git checkout . &&
34+
35+
mkdir dir &&
36+
echo "[a]b=c" >file1 &&
37+
echo "[x]y=z" >dir/file2 &&
38+
39+
# MSYS2 is very forgiving, it will resolve symlinks even if the
40+
# symlink type is incorrect. To make this test meaningful, try
41+
# them with a native, non-MSYS executable, such as `git config`.
42+
test "$(git config -f file-link a.b)" = "c" &&
43+
test "$(git config -f dir-link/file2 x.y)" = "z"
44+
'
45+
46+
test_done

0 commit comments

Comments
 (0)