From 82cc187b952b8d6539bcb72ea5c63469409c919a Mon Sep 17 00:00:00 2001 From: Michael Geddes Date: Wed, 14 May 2014 13:59:48 +0800 Subject: [PATCH 01/14] MinGW: Add symlink support for NTFS on windows This patch implements git support for NTFS symbolic link type reparse points. * There is a specific privelege required to create symbolic links that is not generally associated with a standard user. This part is up to the user to worry about. * NTFS reparse points differentiate between file and directory links. This patch assumes file links are meant. (A separate patch will develop this further). * This patch is not intended to implement symbolic links in the shell utilities. This means that as of when this was written, bash and gnu utilities do not handle them. * Windows chdir behaves differently to *nix, and we need to unravel symbolic links for various operations to work as expected. * For efficiency, as much as possible of the calls are done with wchar_t, before being converted to utf-8. This is as much about avoiding dealing with windows default encoding as anything else. * resolve_symlink needed to be replaced in lockfile.c since there are some issues with recognising absolute paths, as well as for efficiency with wchar_t. This work was based on a combination of patches developed by the following people: original-by: Johannes Schindelin original-by: Thorvald Natvig Signed-off-by: Michael Geddes --- compat/mingw.c | 521 ++++++++++++++++++++++++++++++++++++++---- compat/mingw.h | 9 +- compat/win32/dirent.c | 9 +- lockfile.c | 6 + 4 files changed, 497 insertions(+), 48 deletions(-) diff --git a/compat/mingw.c b/compat/mingw.c index d87a0e03b6e902..7bc7fff8f7be1e 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -1,5 +1,6 @@ #include "../git-compat-util.h" #include "win32.h" +#include #include #include #include "../strbuf.h" @@ -201,16 +202,46 @@ static int ask_yes_no_if_possible(const char *format, ...) } } -int mingw_unlink(const char *pathname) +int do_wunlink(const wchar_t *wpathname) { - int ret, tries = 0; - wchar_t wpathname[MAX_LONG_PATH]; - if (xutftowcs_long_path(wpathname, pathname) < 0) + int ret, tries; + WIN32_FIND_DATAW findbuf; + HANDLE handle; + + /* Check for directories and symlinks */ + handle = FindFirstFileW(wpathname, &findbuf); + if (handle == INVALID_HANDLE_VALUE) { + errno = ENOENT; return -1; + } + FindClose(handle); /* read-only files cannot be removed */ _wchmod(wpathname, 0666); - while ((ret = _wunlink(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) { + + tries = 0; + + do { + if (findbuf.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) { + BOOL bres; + if (findbuf.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + bres = RemoveDirectoryW(wpathname); + else + bres = DeleteFileW(wpathname); + if (!bres) + ret = -1; + else + ret = 0; + } + else if (findbuf.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + ret = _wrmdir(wpathname); + } + else + ret = _wunlink(wpathname); + + if (ret == 0) + break; + if (!is_file_in_use_error(GetLastError())) break; /* @@ -220,16 +251,61 @@ int mingw_unlink(const char *pathname) * complete its operation, we give up our time slice now. * If we have to retry again, we do sleep a bit. */ + if (tries >= ARRAY_SIZE(delay)) + break; Sleep(delay[tries]); tries++; - } - while (ret == -1 && is_file_in_use_error(GetLastError()) && + } while (TRUE); + + return ret; +} + +int mingw_wunlink(const wchar_t *wpathname) +{ + char pathname[PATH_MAX]; + int ret; + + do { + + ret = do_wunlink(wpathname); + + if (ret == 0) + break; + + if (xwcstoutf(pathname, wpathname, PATH_MAX) < 0) + return -1; + + } while (is_file_in_use_error(GetLastError()) && ask_yes_no_if_possible("Unlink of file '%s' failed. " - "Should I try again?", pathname)) - ret = _wunlink(wpathname); + "Should I try again?", pathname)); + return ret; + + } +static inline wchar_t *to_backslash_wpath(wchar_t *path); + +int mingw_unlink(const char *pathname) +{ + wchar_t wpathname[MAX_LONG_PATH]; + int ret; + + do { + + if (xutftowcs_long_path(wpathname, pathname) < 0) + return -1; + + ret = do_wunlink(to_backslash_wpath(wpathname)); + + } while (ret != 0 && is_file_in_use_error(GetLastError()) && + ask_yes_no_if_possible("Unlink of file '%s' failed. " + "Should I try again?", pathname)); + + return ret; +} + + static int is_dir_empty(const wchar_t *wpath) { WIN32_FIND_DATAW findbuf; @@ -258,6 +334,7 @@ int mingw_rmdir(const char *pathname) wchar_t wpathname[MAX_LONG_PATH]; if (xutftowcs_long_path(wpathname, pathname) < 0) return -1; + to_backslash_wpath(wpathname); while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) { if (!is_file_in_use_error(GetLastError())) @@ -461,6 +538,46 @@ int mingw_access(const char *filename, int mode) return _waccess(wfilename, mode & ~X_OK); } +static int do_wlstat(int follow, const wchar_t *wfilename, struct stat *buf, wchar_t *wbuffer, int buffersize); +static int do_readlink(const wchar_t *path, wchar_t *buf, size_t bufsiz); +static wchar_t *do_resolve_symlink(wchar_t *pathname, size_t bufsize); +static inline int is_absolute_pathw(const wchar_t *path); +static wchar_t *do_getcwd(wchar_t *wpointer, int len); + +/* + * When changing to a directory that contains symbolic links in the path, + * we need to follow the unix behaviour, which is to unravel the symbolic links. + * + * Windows seems to leave the symbolic links intact. + * This breaks functions like make_absolute_path() that requires the unix behaviour to work. + * + */ +static int do_wchdir(wchar_t *dirname) +{ + wchar_t resolved[MAX_PATH]; + int ret; + + if (! is_absolute_pathw(dirname)) { + + /* + * Change to real, symlink-resolved, CWD first. + * This enforces unix behaviour when CWD is a symlink. + */ + if (!do_getcwd(resolved, MAX_PATH)) { + errno = ENOENT; /* CWD is not a path. */ + return -1; + } + + ret = _wchdir(resolved); + if (ret) + return ret; + } + wcscpy(resolved, dirname); + do_resolve_symlink(resolved, MAX_PATH); + return _wchdir(resolved); +} + + /* cached length of current directory for handle_long_path */ static int current_directory_len = 0; @@ -471,7 +588,7 @@ int mingw_chdir(const char *dirname) /* SetCurrentDirectoryW doesn't support long paths */ if (xutftowcs_path(wdirname, dirname) < 0) return -1; - result = _wchdir(wdirname); + return do_wchdir(to_backslash_wpath(wdirname)); current_directory_len = GetCurrentDirectoryW(0, NULL); return result; } @@ -484,21 +601,187 @@ int mingw_chmod(const char *filename, int mode) return _wchmod(wfilename, mode); } -/* We keep the do_lstat code in a separate function to avoid recursion. - * When a path ends with a slash, the stat will fail with ENOENT. In - * this case, we strip the trailing slashes and stat again. +#ifndef FSCTL_GET_REPARSE_POINT +#define FSCTL_GET_REPARSE_POINT 0x000900a8 +#endif + +static int do_readlink(const wchar_t *path, wchar_t *buf, size_t bufsiz) +{ + HANDLE handle = CreateFileW(path, GENERIC_READ, + FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, + NULL); + + if (handle != INVALID_HANDLE_VALUE) { + unsigned char buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE]; + DWORD dummy = 0; + if (DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0, buffer, + MAXIMUM_REPARSE_DATA_BUFFER_SIZE, &dummy, NULL)) { + REPARSE_DATA_BUFFER *b = (REPARSE_DATA_BUFFER *) buffer; + if (b->ReparseTag == IO_REPARSE_TAG_SYMLINK) { + int len = b->SymbolicLinkReparseBuffer.SubstituteNameLength / sizeof(wchar_t); + int offset = b->SymbolicLinkReparseBuffer.SubstituteNameOffset / sizeof(wchar_t); + len = (bufsiz < len) ? bufsiz : len; + /* Get rid of the \??\ prefix that gets put into absolute reparse points, something + * to do with the NT drive namespace. + */ + if (len >4 && wcsncmp(b->SymbolicLinkReparseBuffer.PathBuffer+offset,L"\\??\\",4) == 0) { + offset += 4; + len -= 4; + } + wcsncpy(buf, & b->SymbolicLinkReparseBuffer.PathBuffer[offset], len); + buf[len] = 0; + CloseHandle(handle); + return len; + } + } + + CloseHandle(handle); + } + + errno = EINVAL; + return -1; +} + +#define has_dos_drive_prefixw(path) (iswalpha(*(path)) && (path)[1] == ':') +static inline int is_absolute_pathw(const wchar_t *path) +{ + return is_dir_sep(path[0]) || has_dos_drive_prefixw(path); +} + +char *mingw_resolve_symlink(char *pathname, size_t bufsize) { + wchar_t wpathname[MAX_PATH+1]; + if (xutftowcs(wpathname, pathname, MAX_PATH) >= 0) { + do_resolve_symlink(to_backslash_wpath(wpathname), MAX_PATH); + xwcstoutf(pathname, wpathname, bufsize); + } + return pathname; +} + +/* + * This resolves symlinks with support for symlinks in the path along the way. * - * If follow is true then act like stat() and report on the link - * target. Otherwise report on the link itself. + * It is based on resolve_symlink in lockfile.c with support for widechars. + * + * To do this, it uses a left-to-right approach, checking each part of the path + * for a symlink and then replacing that section (depending on whether it is + * absolute or relative) with the link. + * + * This implementation is required to pass tests for make_absolute_path. */ -static int do_lstat(int follow, const char *file_name, struct stat *buf) +const int MAXDEPTH = 10; +static wchar_t *do_resolve_symlink(wchar_t *pathname, size_t bufsize) +{ + /* Limit the number of links we resolve to prevent recursion */ + int depth = MAXDEPTH; + + wchar_t *start=pathname; + wchar_t *from=pathname; + wchar_t *last; + wchar_t link[MAX_PATH+1]; + + while (*from && depth > 0) { + wchar_t endch; + int link_len; + /* Find the next section. */ + from = wcschr(start+1, '\\'); + if (!from) + from = start+wcslen(start); + else { + /* Skip drive letters */ + if (from > start && from[-1] == ':') { + from = wcschr(from+1, '\\'); + if (!from) + from = start+wcslen(start); + } + } + if (start > pathname && from-start == 3 && start[1]=='.' && start[2] == '.' ) { + /* Handle /../ */ + if (start[-1] != ':') { + for (last = start-1; last > pathname; --last) + if (*last == '\\') + break; + if (last > pathname) { + memmove(last, from, (pathname-from)+bufsize ); + } + } + } + + /* Temporarily replace pathsep with \0 */ + endch = *from; + *from = L'\0'; + /* and read the link up to that point */ + link_len = do_readlink(pathname, link, MAX_PATH); + *from = endch; + + if (link_len >=0) { + /* It's a link - make sure it will fit */ + if ((link_len + (from-pathname)) > MAX_PATH) { + char path[MAX_PATH]; + xwcstoutf(path, pathname, MAX_PATH); + warning("%s: symlink too long", path); + return pathname; + } + /* readlink() never null-terminates */ + link[link_len] = L'\0'; + + if (is_absolute_pathw(link)) { + /* Concat rest onto link */ + wcscat(link, from); + if (wcslen(link) > bufsize) { + char path[MAX_PATH]; + xwcstoutf(path, pathname, MAX_PATH); + warning("%s: symlink too long", path); + return pathname; + } + /* Absolute path replace all*/ + wcscpy(pathname, link); + start = pathname; + } else { + if ((wcslen(link)+(start-pathname) > bufsize)) { + char path[MAX_PATH]; + xwcstoutf(path, pathname, MAX_PATH); + warning("%s: symlink too long", path); + return pathname; + } + /* Concat rest onto link */ + wcscat(link, from); + + if (start[0] == L'.' && start[1] == L'\0') { + /* Special case of '.' and a relative symlink, + since this has to be actually relative to + one directory up. + */ + wcscpy(start, L"..\\"); + /* Concat with link and rest of filename. + */ + wcscpy(start+3, link); + } else { + /* replace bit from start + * with link and rest of filename. + */ + wcscpy(start, link); + } + } + --depth; + } else { + /* Move to next section. + */ + start = from+1; + while (*start == '\\') + ++start; + } + } + return pathname; +} + +static int do_wlstat(int follow, const wchar_t *wfilename, struct stat *buf, wchar_t *wbuffer, int buffersize) { WIN32_FILE_ATTRIBUTE_DATA fdata; - wchar_t wfilename[MAX_LONG_PATH]; - if (xutftowcs_long_path(wfilename, file_name) < 0) - return -1; + int usebuffer = 0; - if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) { + while (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) { buf->st_ino = 0; buf->st_gid = 0; buf->st_uid = 0; @@ -512,24 +795,54 @@ static int do_lstat(int follow, const char *file_name, struct stat *buf) buf->st_ctime = filetime_to_time_t(&(fdata.ftCreationTime)); if (fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) { WIN32_FIND_DATAW findbuf; + int len; + /* Check for trailing pathsep and remove it. */ + len = wcslen(wfilename); + if (len > 0 && wfilename[len-1] == L'\\') { + if (!usebuffer) { + /* don't modify original */ + usebuffer = 1; + wcscpy(wbuffer, wfilename); + /* Now continuing using passed-in buffer. */ + wfilename = wbuffer; + } + wbuffer[len-1] = L'\0'; + } + HANDLE handle = FindFirstFileW(wfilename, &findbuf); - if (handle != INVALID_HANDLE_VALUE) { + if (handle == INVALID_HANDLE_VALUE) { + /* Prevent infinite loop */ + break; + } else { if ((findbuf.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) && (findbuf.dwReserved0 == IO_REPARSE_TAG_SYMLINK)) { - if (follow) { - char buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE]; - buf->st_size = readlink(file_name, buffer, MAXIMUM_REPARSE_DATA_BUFFER_SIZE); + if (!follow || !wbuffer) { + wchar_t buffer[MAX_PATH]; + buf->st_size = do_readlink(wfilename, buffer, MAX_PATH); + buf->st_mode = S_IFLNK; + buf->st_mode |= S_IREAD; + if (!(findbuf.dwFileAttributes & FILE_ATTRIBUTE_READONLY)) + buf->st_mode |= S_IWRITE; + FindClose(handle); + return 0; } else { buf->st_mode = S_IFLNK; + wcscpy(wbuffer, wfilename); + if (do_resolve_symlink(wbuffer, buffersize) <= 0) { + FindClose(handle); + break; + } + /* Now continuing using link in passed-in buffer. + */ + wfilename = wbuffer; + usebuffer = 1; } - buf->st_mode |= S_IREAD; - if (!(findbuf.dwFileAttributes & FILE_ATTRIBUTE_READONLY)) - buf->st_mode |= S_IWRITE; } FindClose(handle); } } - return 0; + else + return 0; } switch (GetLastError()) { case ERROR_ACCESS_DENIED: @@ -551,6 +864,22 @@ static int do_lstat(int follow, const char *file_name, struct stat *buf) return -1; } +/* We keep the do_lstat code in a separate function to avoid recursion. + * When a path ends with a slash, the stat will fail with ENOENT. In + * this case, we strip the trailing slashes and stat again. + * + * If follow is true then act like stat() and report on the link + * target. Otherwise report on the link itself. + */ +static int do_lstat(int follow, const char *file_name, struct stat *buf) +{ + wchar_t wfilename[MAX_LONG_PATH]; + if (xutftowcs_long_path(wfilename, file_name) < 0) + return -1; + to_backslash_wpath(wfilename); + return do_wlstat(follow, wfilename, buf, wfilename, MAX_PATH); +} + /* We provide our own lstat/fstat functions, since the provided * lstat/fstat functions are so slow. These stat functions are * tailored for Git's usage (read: fast), and are not meant to be @@ -755,17 +1084,35 @@ struct tm *localtime_r(const time_t *timep, struct tm *result) return result; } -char *mingw_getcwd(char *pointer, int len) +static wchar_t *do_getcwd(wchar_t *wpointer, int len) { int i; + if (!_wgetcwd(wpointer, len)) + return NULL; + + /* Unix getcwd resolves symlinks + */ + do_resolve_symlink(wpointer, len); + i = wcslen(wpointer); + /* Unix getcwd doesn't appear ever to have a trailing / + * which do_resolve_symlink can append. + */ + if (wpointer[i-1] == L'\\') + wpointer[i-1] = L'\0'; + return wpointer; +} + +static inline wchar_t *to_unix_wpath(wchar_t *path); + +char *mingw_getcwd(char *pointer, int len) +{ wchar_t wpointer[MAX_PATH]; - if (!_wgetcwd(wpointer, ARRAY_SIZE(wpointer))) + if (!do_getcwd(wpointer, ARRAY_SIZE(wpointer))) return NULL; - if (xwcstoutf(pointer, wpointer, len) < 0) + + if (xwcstoutf(pointer, to_unix_wpath(wpointer), len) < 0) return NULL; - for (i = 0; pointer[i]; i++) - if (pointer[i] == '\\') - pointer[i] = '/'; + return pointer; } @@ -1856,25 +2203,49 @@ int mingw_raise(int sig) } } +static inline wchar_t *to_unix_wpath(wchar_t *path) +{ + wchar_t *c; + for (c = path; *c; c++) { + if (*c == '\\') + *c = '/'; + } + return path; +} + +static inline wchar_t *to_backslash_wpath(wchar_t *path) +{ + wchar_t *c; + for (c = path; *c; c++) { + if (*c == '/') + *c = '\\'; + } + return path; +} + -static const char *make_backslash_path(const char *path) +static inline char *backslash_path(char *path) { - static char buf[PATH_MAX + 1]; char *c; + for (c = path; *c; c++) { + if (*c == '/') + *c = '\\'; + } + return path; +} +static const char *make_backslash_path(const char *path, char *buf) +{ if (strlcpy(buf, path, PATH_MAX) >= PATH_MAX) die("Too long path: %.*s", 60, path); - for (c = buf; *c; c++) { - if (*c == '/') - *c = '\\'; - } - return buf; + return backslash_path(buf); } void mingw_open_html(const char *unixpath) { - const char *htmlpath = make_backslash_path(unixpath); + char buf[PATH_MAX + 1]; + const char *htmlpath = make_backslash_path(unixpath, buf); typedef HINSTANCE (WINAPI *T)(HWND, const char *, const char *, const char *, const char *, INT); T ShellExecute; @@ -1901,10 +2272,21 @@ int link(const char *oldpath, const char *newpath) { typedef BOOL (WINAPI *T)(LPCWSTR, LPCWSTR, LPSECURITY_ATTRIBUTES); static T create_hard_link = NULL; - wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH]; + wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH], wbuf[MAX_LONG_PATH]; + struct stat st; + if (xutftowcs_long_path(woldpath, oldpath) < 0 || xutftowcs_long_path(wnewpath, newpath) < 0) return -1; + to_backslash_wpath(woldpath); + to_backslash_wpath(wnewpath); + + if (!do_wlstat(0, wnewpath, &st, wbuf, MAX_PATH)) { + /* Delete the file if it exists. + */ + if (mingw_wunlink(wnewpath)) + return -1; + } if (!create_hard_link) { create_hard_link = (T) GetProcAddress( @@ -1923,6 +2305,59 @@ int link(const char *oldpath, const char *newpath) return 0; } +int symlink(const char *oldpath, const char *newpath) +{ + typedef BOOL WINAPI (*symlink_fn)(const wchar_t*, const wchar_t*, DWORD); + static symlink_fn create_symbolic_link = NULL; + wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH], wbuf[MAX_PATH]; + struct stat st; + + if (xutftowcs(woldpath, oldpath, MAX_PATH) < 0) + return -1; + if (xutftowcs(wnewpath, newpath, MAX_PATH) < 0) + return -1; + to_backslash_wpath(woldpath); + to_backslash_wpath(wnewpath); + if (!do_wlstat(0, wnewpath, &st, wbuf, MAX_PATH)) { + /* Delete the file if it exists. + */ + if (mingw_wunlink(wnewpath)) + return -1; + } + + if (!create_symbolic_link) { + create_symbolic_link = (symlink_fn) GetProcAddress( + GetModuleHandle("kernel32.dll"), "CreateSymbolicLinkW"); + if (!create_symbolic_link) + create_symbolic_link = (symlink_fn)-1; + } + if (create_symbolic_link == (symlink_fn)-1) { + errno = ENOSYS; + return -1; + } + + if (!create_symbolic_link(wnewpath, woldpath, 0)) { + errno = err_win_to_posix(GetLastError()); + return -1; + } + return 0; +} + +int do_readlink(const wchar_t *path, wchar_t *buf, size_t bufsiz); + +int readlink(const char *path, char *buf, size_t bufsiz) +{ + wchar_t wpath[MAX_PATH], wbuffer[MAX_PATH]; + int result; + if (xutftowcs(wpath, path, MAX_PATH) < 0) + return -1; + result = do_readlink(wpath, wbuffer, MAX_PATH); + if (result >= 0) { + return xwcstoutf(buf, to_unix_wpath(wbuffer), bufsiz); + } + return -1; +} + pid_t waitpid(pid_t pid, int *status, int options) { HANDLE h = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION, diff --git a/compat/mingw.h b/compat/mingw.h index 08b83fe1ded988..8be34d4890337e 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -84,10 +84,6 @@ struct itimerval { * trivial stubs */ -static inline int readlink(const char *path, char *buf, size_t bufsiz) -{ errno = ENOSYS; return -1; } -static inline int symlink(const char *oldpath, const char *newpath) -{ errno = ENOSYS; return -1; } static inline int fchmod(int fildes, mode_t mode) { errno = ENOSYS; return -1; } static inline pid_t fork(void) @@ -163,6 +159,8 @@ struct passwd *getpwuid(uid_t uid); 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 symlink(const char *oldpath, const char *newpath); +int readlink(const char *path, char *buf, size_t bufsiz); /* * replacements of existing functions @@ -376,6 +374,9 @@ void mingw_open_html(const char *path); void mingw_mark_as_git_dir(const char *dir); #define mark_as_git_dir mingw_mark_as_git_dir +char *mingw_resolve_symlink(char *p, size_t s); +#define resolve_symlink mingw_resolve_symlink + /** * Max length of long paths (exceeding MAX_PATH). The actual maximum supported * by NTFS is 32,767 (* sizeof(wchar_t)), but we choose an arbitrary smaller diff --git a/compat/win32/dirent.c b/compat/win32/dirent.c index b3bd8d7af77291..8cf96ff42b52fb 100644 --- a/compat/win32/dirent.c +++ b/compat/win32/dirent.c @@ -16,7 +16,14 @@ static inline void finddata2dirent(struct dirent *ent, WIN32_FIND_DATAW *fdata) xwcstoutf(ent->d_name, fdata->cFileName, MAX_PATH * 3); /* Set file type, based on WIN32_FIND_DATA */ - if (fdata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + /* First check for symlinks since a directory symlink has the FILE_ATTRIBUTE_DIRECTORY + * attribute as well. Posix doesn't distinguish between directory/file symlinks, but + * NTFS does. + */ + if (fdata->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT + && (fdata->dwReserved0 == IO_REPARSE_TAG_SYMLINK)) + ent->d_type = DT_LNK; + else if (fdata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ent->d_type = DT_DIR; else ent->d_type = DT_REG; diff --git a/lockfile.c b/lockfile.c index 8fbcb6a98aae85..a812d0605237dc 100644 --- a/lockfile.c +++ b/lockfile.c @@ -29,6 +29,11 @@ static void remove_lock_file_on_signal(int signo) raise(signo); } +/* mingw requires its own version of resolve_symlink to be use, + * including in lock_file below + */ +#ifndef resolve_symlink + /* * p = absolute or relative path name * @@ -121,6 +126,7 @@ static char *resolve_symlink(char *p, size_t s) return p; } +#endif static int lock_file(struct lock_file *lk, const char *path, int flags) { From 75b8a678eed3282cf3ae45624b950c22a3eff136 Mon Sep 17 00:00:00 2001 From: Michael Geddes Date: Thu, 27 Dec 2012 08:43:37 +0800 Subject: [PATCH 02/14] MinGW: Allow passing the symlink target type from index information. Required for msysgit which needs to know whether the target of a symbolic link is a directory or file. Signed-off-by: Michael Geddes --- entry.c | 125 +++++++++++++++++++++++++++++++++++++++++++++- git-compat-util.h | 10 ++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/entry.c b/entry.c index 77c688262477e7..9c39003c0ac140 100644 --- a/entry.c +++ b/entry.c @@ -136,6 +136,126 @@ static int streaming_write_entry(const struct cache_entry *ce, char *path, return result; } +/* + * Does 'match' match the given name? + * A match is found if + * + * (1) the 'match' string is leading directory of 'name', or + * (2) the 'match' string is exactly the same as 'name'. + * + * and the return value tells which case it was. + * + * It returns 0 when there is no match. + * + * Preserved and simplified from dir.c for use here (without glob special matching) + */ +static int match_one(const char *match, const char *name, int namelen) +{ + int matchlen; + + /* If the match was just the prefix, we matched */ + if (!*match) + return MATCHED_RECURSIVELY; + + if (ignore_case) { + for (;;) { + unsigned char c1 = tolower(*match); + unsigned char c2 = tolower(*name); + if (c1 == '\0' ) + break; + if (c1 != c2) + return 0; + match++; + name++; + namelen--; + } + /* We don't match the matchstring exactly, */ + matchlen = strlen(match); + if (strncmp_icase(match, name, matchlen)) + return 0; + } else { + for (;;) { + unsigned char c1 = *match; + unsigned char c2 = *name; + if (c1 == '\0' ) + break; + if (c1 != c2) + return 0; + match++; + name++; + namelen--; + } + /* We don't match the matchstring exactly, */ + matchlen = strlen(match); + if (strncmp(match, name, matchlen)) + return 0; + } + + if (namelen == matchlen) + return MATCHED_EXACTLY; + if (match[matchlen-1] == '/' || name[matchlen] == '/') + return MATCHED_RECURSIVELY; + return 0; +} + +static enum git_target_type get_symlink_type(const char *filepath, const char *symlinkpath) +{ + /* For certain O/S and file-systems, symlinks need to know before-hand whether it + * is a directory or a file being pointed to. + * + * This allows us to use index information for relative paths that lie + * within the working directory. + * + * This function is not interested in interrogating the file-system. + */ + char *sanitized; + const char *fpos, *last; + enum git_target_type ret; + int len, pos; + + /* This is an absolute path, so git doesn't know. + */ + if (is_absolute_path(symlinkpath)) + return GIT_TARGET_UNKNOWN; + + /* Work on a sanitized version of the path that can be + * matched against the index. + */ + last = NULL; + for (fpos = filepath; *fpos; ++fpos) + if (is_dir_sep(*fpos)) + last = fpos; + + if (last) { + len = (1+last-filepath); + sanitized = xmalloc(len + strlen(symlinkpath)+1); + memcpy(sanitized, filepath, 1+last-filepath); + } else { + len = 0; + sanitized = xmalloc(strlen(symlinkpath)+1); + } + strcpy(sanitized+len, symlinkpath); + + ret = GIT_TARGET_UNKNOWN; + if (!normalize_path_copy(sanitized, sanitized)) { + for (pos = 0; pos < active_nr; pos++) { + struct cache_entry *ce = active_cache[pos]; + switch (match_one(sanitized, ce->name, ce_namelen(ce))) { + case MATCHED_EXACTLY: + case MATCHED_FNMATCH: + ret = GIT_TARGET_ISFILE; + break; + case MATCHED_RECURSIVELY: + ret = GIT_TARGET_ISDIR; + break; + } + } + } + + free(sanitized); + return ret; +} + static int write_entry(struct cache_entry *ce, char *path, const struct checkout *state, int to_tempfile) { @@ -165,7 +285,10 @@ static int write_entry(struct cache_entry *ce, path, sha1_to_hex(ce->sha1)); if (ce_mode_s_ifmt == S_IFLNK && has_symlinks && !to_tempfile) { - ret = symlink(new, path); + /* Note that symlink_with_type is a macro, and that for filesystems that + * don't care, get_symlink_type will not be called. + */ + ret = symlink_with_type(new, path, get_symlink_type(path, new)); free(new); if (ret) return error("unable to create symlink %s (%s)", diff --git a/git-compat-util.h b/git-compat-util.h index bf7bd41faea929..e0f258c31017ea 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -85,6 +85,16 @@ #define _NETBSD_SOURCE 1 #define _SGI_SOURCE 1 +/* default is not to pass type - mingw needs this */ +#define symlink_with_type(a,b,c) symlink((a),(b)) + +/* Used for 'Target Type' Parameter for symlink_with_type */ +enum git_target_type { + GIT_TARGET_UNKNOWN, + GIT_TARGET_ISFILE, + GIT_TARGET_ISDIR +}; + #if defined(WIN32) && !defined(__CYGWIN__) /* Both MinGW and MSVC */ # if defined (_MSC_VER) && !defined(_WIN32_WINNT) # define _WIN32_WINNT 0x0502 From eabcbcca480d38f62c40255c77e98bf26b4126c7 Mon Sep 17 00:00:00 2001 From: Michael Geddes Date: Wed, 14 May 2014 14:12:14 +0800 Subject: [PATCH 03/14] mingw: Create directory/file symlink from information available Windows NTFS symbolic links require specifying whether the target is a file or directory at creation time. This can partially be done by interrogating the filesystem, however when creating symbolic links that are within a repository, there can be no guarantee of the order of creation. This patch enables the code to allow interrogation of the git cache to determine whether a target will be a file or a directory. Signed-off-by: Michael Geddes --- compat/mingw.c | 58 ++++++++++++++++++++++++++++++++++++++++++++++++-- compat/mingw.h | 7 +++++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/compat/mingw.c b/compat/mingw.c index 7bc7fff8f7be1e..665ff16a646385 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -2268,6 +2268,8 @@ void mingw_open_html(const char *unixpath) } } +#define SYMBOLIC_LINK_FLAG_DIRECTORY 0x1 + int link(const char *oldpath, const char *newpath) { typedef BOOL (WINAPI *T)(LPCWSTR, LPCWSTR, LPSECURITY_ATTRIBUTES); @@ -2305,12 +2307,13 @@ int link(const char *oldpath, const char *newpath) return 0; } -int symlink(const char *oldpath, const char *newpath) +int mingw_symlink(const char *oldpath, const char *newpath, enum git_target_type targettype) { typedef BOOL WINAPI (*symlink_fn)(const wchar_t*, const wchar_t*, DWORD); static symlink_fn create_symbolic_link = NULL; wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH], wbuf[MAX_PATH]; struct stat st; + int flags = 0; if (xutftowcs(woldpath, oldpath, MAX_PATH) < 0) return -1; @@ -2325,6 +2328,57 @@ int symlink(const char *oldpath, const char *newpath) return -1; } + switch (targettype) { + case GIT_TARGET_UNKNOWN: + { + /* Determine the target symbolic link type from the + Filesystem. + */ + wchar_t wcurdir[MAX_PATH] = L""; + if (!is_absolute_pathw(woldpath)) { + /* If woldpath is relative, then stat needs to be + from the directory containing the original file. + */ + + wchar_t *pos, *wlast=NULL, oldc; + int ret; + + for (pos = wnewpath; *pos; ++pos) + if (is_dir_sep(*pos)) + wlast = pos; + + if (wlast != NULL) { + do_getcwd(wcurdir, MAX_PATH); + + oldc = *wlast; + *wlast = L'\0'; + + ret = do_wchdir(wnewpath); + if (ret) + *wcurdir = L'\0'; + + *wlast = oldc; + if (ret) + return -1; + } + } + + if (!do_wlstat(1, woldpath, &st, wbuf, MAX_PATH)) { + if (S_ISDIR(st.st_mode) ) + flags = SYMBOLIC_LINK_FLAG_DIRECTORY; + } + + if (*wcurdir) + do_wchdir(wcurdir); + } + break; + case GIT_TARGET_ISDIR: + flags = SYMBOLIC_LINK_FLAG_DIRECTORY; + break; + case GIT_TARGET_ISFILE: + break; + } + if (!create_symbolic_link) { create_symbolic_link = (symlink_fn) GetProcAddress( GetModuleHandle("kernel32.dll"), "CreateSymbolicLinkW"); @@ -2336,7 +2390,7 @@ int symlink(const char *oldpath, const char *newpath) return -1; } - if (!create_symbolic_link(wnewpath, woldpath, 0)) { + if (!create_symbolic_link(wnewpath, woldpath, flags)) { errno = err_win_to_posix(GetLastError()); return -1; } diff --git a/compat/mingw.h b/compat/mingw.h index 8be34d4890337e..9dbba85937d99c 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -144,6 +144,10 @@ static inline int mingw_SSL_set_wfd(SSL *ssl, int fd) #define SSL_set_wfd mingw_SSL_set_wfd #endif +#undef symlink_with_type +#define symlink_with_type(a,b,c) mingw_symlink((a),(b),(c)) +#define symlink(a,b) mingw_symlink((a),(b),GIT_TARGET_UNKNOWN) + /* * implementations of missing functions */ @@ -159,7 +163,8 @@ struct passwd *getpwuid(uid_t uid); 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 symlink(const char *oldpath, const char *newpath); + +int mingw_symlink(const char *oldpath, const char *newpath, enum git_target_type targettype); int readlink(const char *path, char *buf, size_t bufsiz); /* From 4d733b96311256c91f4f28d115af699ab67730dd Mon Sep 17 00:00:00 2001 From: Michael Geddes Date: Sat, 26 Apr 2014 13:34:45 +0800 Subject: [PATCH 04/14] test: Work around lack of windows native symlink support in current msys The current msys does not have support for NTFS symlinks. This includes in 'ln', 'test', 'rm'. Here the commands are overridden to use windows cmd.exe instead of msys. The implementation of test is quite expensive but works for the test. 'rm' needs to handle many scenarios. Symlinks are not supported at all by the msys version. * 'rm -rf' equivalent needs to first delete the files, and then the directories. * Directory symbolic links need to use cmd.exe 'rmdir' * file symbolic links need to use 'del'. * A couple of places use 'rm -rf' on a mix of files and directories, so needs to be split up. Signed-off-by: Michael Geddes --- t/t0050-filesystem.sh | 2 +- t/t4004-diff-rename-symlink.sh | 4 +- t/test-lib.sh | 101 +++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 2 deletions(-) diff --git a/t/t0050-filesystem.sh b/t/t0050-filesystem.sh index 6b3cedcf24613b..056dde4d163e71 100755 --- a/t/t0050-filesystem.sh +++ b/t/t0050-filesystem.sh @@ -45,7 +45,7 @@ test_expect_success "detection of filesystem w/o symlink support during repo ini test "$(git config --bool core.symlinks)" = true ' else -test_expect_success "detection of filesystem w/o symlink support during repo init" ' +test_expect_failure "detection of filesystem w/o symlink support during repo init" ' v=$(git config --bool core.symlinks) && test "$v" = false ' diff --git a/t/t4004-diff-rename-symlink.sh b/t/t4004-diff-rename-symlink.sh index 6e562c80d12f9f..1175dd5b9ec948 100755 --- a/t/t4004-diff-rename-symlink.sh +++ b/t/t4004-diff-rename-symlink.sh @@ -22,8 +22,10 @@ test_expect_success SYMLINKS \ test_expect_success SYMLINKS \ 'prepare work tree' \ - 'mv frotz rezrov && + '>xyzzy && + mv frotz rezrov && rm -f yomin && + rm -f xyzzy && ln -s xyzzy nitfol && ln -s xzzzy bozbar && git update-index --add --remove frotz rezrov nitfol bozbar yomin' diff --git a/t/test-lib.sh b/t/test-lib.sh index cc74bafa6200f2..77f7d44d51eea5 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -694,6 +694,63 @@ case "$TRASH_DIRECTORY" in *) TRASH_DIRECTORY="$TEST_OUTPUT_DIRECTORY/$TRASH_DIRECTORY" ;; esac test ! -z "$debug" || remove_trash=$TRASH_DIRECTORY + +case $(uname -s) in +*MINGW*) + # Converting to win32 path to supply to cmd.exe command. + winpath () { + # pwd -W is the only way of getting msys to convert the path, especially mounted paths like /usr + # since cmd /c only takes a single parameter preventing msys automatic path conversion. + if builtin test "${1:~-1:1}" != "/" ; then + echo "$1" | sed 's+/+\\+g' + elif builtin test -d "$1" ; then + (cd "$1"; pwd -W) | sed 's+/+\\+g' + elif builtin test -d "${1%/*}" ; then + (cd "${1%/*}"; echo "$(pwd -W)/${1##*/}") | sed 's+/+\\+g' + else + echo "$1" | sed -e 's+^/\([a-z]\)/+\1:/+' -e 's+/+\\+g' + fi + } + rm () { + rm_is_f=0 + rm_is_r=0 + for ARG in "$@" + do + case "$ARG" in + -rf|-fr) rm_is_r=1 ; rm_is_f=1 ;; + -f) rm_is_f=1 ;; + -r) rm_is_r=1 ;; + -*) ;; # ignore + *) + rm_arg_winpath="$(winpath "$ARG")" + if test -d "$ARG" ; then + if test $rm_is_r -eq 1 ; then + # Delete files, then remove directories. + # Force so make sure the result is true + cmd /c "del /s/q/f \"$rm_arg_winpath\" 2>nul && rmdir /q/s \"$rm_arg_winpath\" 2>nul" >/dev/null || true + else + # Works for symlinks as well + cmd /c "@rmdir /q \"$rm_arg_winpath\"" + fi + builtin test -d "$ARG" && echo "rm: unable to delete \"$ARG\"" 1>&2 && return 1 + else + # Using del works for symlinks as well + if test $rm_is_f -eq 1 ; then + # Force so make sure the result is true + cmd /c "@del /q/f \"$rm_arg_winpath\" 2>nul" >/dev/null || true + else + cmd /c "@del /q/f \"$rm_arg_winpath\" 2>nul" >/dev/null + fi + test -f "$ARG" && echo "rm: unable to delete \"$ARG\"" 1>&2 && return 1 + fi + ;; + esac + done + return 0 + } + ;; +esac + rm -fr "$TRASH_DIRECTORY" || { GIT_EXIT_OK=t echo >&5 "FATAL: Cannot prepare test area" @@ -754,6 +811,49 @@ case $(uname -s) in pwd () { builtin pwd -W } + # use mklink + ln () { + + ln_sym_hard=/H + ln_sym_dir= + if test "$1" = "-s" + then + ln_sym_hard= + shift + fi + pushd $(dirname "$2") 2>&1 > /dev/null + builtin test -d "$1" && ln_sym_dir=/D + popd > /dev/null 2> /dev/null + cmd /c "mklink ${ln_sym_hard}${ln_sym_dir} \"$(winpath "$2")\" \"$(winpath "$1")\">/dev/null " 2>/dev/null + ln_eval_ret=$? + if test $ln_eval_ret != 0 -a -n "$ln_sym_hard" ; then + cp "$1" "$2" + else + return $ln_eval_ret + fi + } + test () { + case "$1" in + -[hL]) + if builtin test -d "$2" ; then + test_sym_dir=$(dirname "$2") + builtin test -n "${test_sym_dir}" && pushd "${test_sym_dir}" 2>&1 > /dev/null + test_sym_base=$(basename "$2") + test_file=$(cmd /c "@dir /b/a:l \"${test_sym_base}?\" 2> nul" | grep "^${test_sym_base}$" ) + builtin test -n "${test_sym_dir}" && popd 2>&1 > /dev/null + builtin test -n "${test_file}" + else + test_file=$(cmd /c "@dir /b/a:l \"$(winpath "$2")\" 2> nul" ) + builtin test -n "${test_file}" + fi + ;; + -f) + test_file=$(cmd /c "@dir /b/a:-d-l-s \"$(winpath "$2")\" 2> nul" ) + builtin test -n "${test_file}" + ;; + *) builtin test "$@";; + esac + } # no POSIX permissions # backslashes in pathspec are converted to '/' # exec does not inherit the PID @@ -837,6 +937,7 @@ test_lazy_prereq PIPE ' test_lazy_prereq SYMLINKS ' # test whether the filesystem supports symbolic links + touch x ln -s x y && test -h y ' From dda0d3d4797f9dd68620624a097017a8e8b8fce1 Mon Sep 17 00:00:00 2001 From: Michael Geddes Date: Tue, 22 Feb 2011 07:31:13 +0800 Subject: [PATCH 05/14] test: Don't have dangling symlinks in tests (for msys) There are 2 reasons for this: * Under windows, ln() must determine whether the target is a directory or a file before creating the link. * msys doesn't handle dangling NTFS symlinks well (for example 'ls' will abort at that point, claiming the file doesn't exist). So I have reordered commands to make sure our symlinks don't dangle from the time of their creation. Signed-off-by: Michael Geddes --- t/t0000-basic.sh | 14 +++++++++++++- t/t1504-ceiling-dirs.sh | 3 ++- t/t2201-add-update-typechange.sh | 3 ++- t/t3010-ls-files-killed-modified.sh | 3 +++ t/t4011-diff-symlink.sh | 3 ++- t/t4023-diff-rename-typechange.sh | 1 + t/t4115-apply-symlink.sh | 4 +++- t/t5000-tar-tree.sh | 1 + 8 files changed, 27 insertions(+), 5 deletions(-) diff --git a/t/t0000-basic.sh b/t/t0000-basic.sh index a2bb63ce8e5e55..258cf396bfbc26 100755 --- a/t/t0000-basic.sh +++ b/t/t0000-basic.sh @@ -429,10 +429,22 @@ test_expect_success 'adding various types of objects with git update-index --add for p in $paths do echo "hello $p" >$p || exit 1 + # Create files for msys + path=${p%/*}/ + if [ "${path}" == "${p}/" ] ; then + path= + fi + linkfile="${path}hello $p" + linkpath="${linkfile%/*}" + if [ "${linkpath}" != "${linkfile}" ] ; then + mkdir -p "${linkpath}" + fi + touch "${linkfile}" + test_ln_s_add "hello $p" ${p}sym || exit 1 done ) && - find path* ! -type d -print | xargs git update-index --add + find path* ! -type d -print | grep -v hello| xargs git update-index --add ' # Show them and see that matches what we expect. diff --git a/t/t1504-ceiling-dirs.sh b/t/t1504-ceiling-dirs.sh index 3d51615e42d53a..b1d2319e796983 100755 --- a/t/t1504-ceiling-dirs.sh +++ b/t/t1504-ceiling-dirs.sh @@ -44,12 +44,13 @@ test_prefix ceil_at_sub "" GIT_CEILING_DIRECTORIES="$TRASH_ROOT/sub/" test_prefix ceil_at_sub_slash "" +mkdir -p sub/dir || exit 1 + if test_have_prereq SYMLINKS then ln -s sub top fi -mkdir -p sub/dir || exit 1 cd sub/dir || exit 1 unset GIT_CEILING_DIRECTORIES diff --git a/t/t2201-add-update-typechange.sh b/t/t2201-add-update-typechange.sh index 954fc51e5b560a..0d98e8bfee75eb 100755 --- a/t/t2201-add-update-typechange.sh +++ b/t/t2201-add-update-typechange.sh @@ -10,6 +10,7 @@ test_expect_success setup ' >yomin && >caskly && if test_have_prereq SYMLINKS; then + touch frotz ln -s frotz nitfol && T_letter=T else @@ -33,13 +34,13 @@ test_expect_success modify ' >nitfol && # rezrov/bozbar disappears rm -fr rezrov && + mkdir xyzzy && if test_have_prereq SYMLINKS; then ln -s xyzzy rezrov else printf %s xyzzy > rezrov fi && # xyzzy disappears (not a submodule) - mkdir xyzzy && echo gnusto >xyzzy/bozbar && # yomin gets replaced with a submodule mkdir yomin && diff --git a/t/t3010-ls-files-killed-modified.sh b/t/t3010-ls-files-killed-modified.sh index 6d3b828a951e4c..9291f179f27314 100755 --- a/t/t3010-ls-files-killed-modified.sh +++ b/t/t3010-ls-files-killed-modified.sh @@ -80,6 +80,9 @@ test_expect_success 'git ls-files -k to show killed files.' ' date >path3 && date >path5 fi && + touch xyzzy + rm path1 + rm xyzzy mkdir -p path0 path1 path6 pathx/ju && date >path0/file0 && date >path1/file1 && diff --git a/t/t4011-diff-symlink.sh b/t/t4011-diff-symlink.sh index 13e7f621ab79f9..1f1dc77f26788e 100755 --- a/t/t4011-diff-symlink.sh +++ b/t/t4011-diff-symlink.sh @@ -31,7 +31,8 @@ test_expect_success 'diff new symlink and file' ' # the empty tree git update-index && tree=$(git write-tree) && - + + touch xyzzy && test_ln_s_add xyzzy frotz && echo xyzzy >nitfol && git update-index --add nitfol && diff --git a/t/t4023-diff-rename-typechange.sh b/t/t4023-diff-rename-typechange.sh index 55d549fcf441be..a4ba0403418460 100755 --- a/t/t4023-diff-rename-typechange.sh +++ b/t/t4023-diff-rename-typechange.sh @@ -8,6 +8,7 @@ test_expect_success setup ' rm -f foo bar && cat "$TEST_DIRECTORY"/../COPYING >foo && + touch linklink test_ln_s_add linklink bar && git add foo && git commit -a -m Initial && diff --git a/t/t4115-apply-symlink.sh b/t/t4115-apply-symlink.sh index 872fcda6cb6dce..3b317abcb907b1 100755 --- a/t/t4115-apply-symlink.sh +++ b/t/t4115-apply-symlink.sh @@ -10,7 +10,8 @@ test_description='git apply symlinks and partial files . ./test-lib.sh test_expect_success setup ' - + mkdir -p path1/path2/path3/path4/ + > path1/path2/path3/path4/path5 test_ln_s_add path1/path2/path3/path4/path5 link1 && git commit -m initial && @@ -18,6 +19,7 @@ test_expect_success setup ' rm -f link? && + > htap6 test_ln_s_add htap6 link1 && git commit -m second && diff --git a/t/t5000-tar-tree.sh b/t/t5000-tar-tree.sh index 1cf0a4e10301fe..c325893f754d37 100755 --- a/t/t5000-tar-tree.sh +++ b/t/t5000-tar-tree.sh @@ -105,6 +105,7 @@ test_expect_success \ printf "A\$Format:%s\$O" "$SUBSTFORMAT" >a/substfile1 && printf "A not substituted O" >a/substfile2 && if test_have_prereq SYMLINKS; then + > a/a ln -s a a/l1 else printf %s a > a/l1 From 4a8368035c88b63afd4fc9352b0897c62475245b Mon Sep 17 00:00:00 2001 From: Michael Geddes Date: Sun, 11 May 2014 09:33:56 +0800 Subject: [PATCH 06/14] test: Factor abspath_of_dir for testing 'absolute paths' Signed-off-by: Michael Geddes --- t/t0060-path-utils.sh | 2 +- t/test-lib.sh | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh index c0143a0a70b7d3..75e1edb3990fde 100755 --- a/t/t0060-path-utils.sh +++ b/t/t0060-path-utils.sh @@ -178,7 +178,7 @@ test_expect_success SYMLINKS 'real path works on symlinks' ' mkdir second && ln -s ../first second/other && mkdir third && - dir="$(cd .git; pwd -P)" && + dir="$(abspath_of_dir .git)" && dir2=third/../second/other/.git && test "$dir" = "$(test-path-utils real_path $dir2)" && file="$dir"/index && diff --git a/t/test-lib.sh b/t/test-lib.sh index 77f7d44d51eea5..6fdd9374103aa4 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -794,6 +794,10 @@ yes () { done } +abspath_of_dir () { + (cd "$1" ; pwd -P) +} + # Fix some commands on Windows case $(uname -s) in *MINGW*) From a662ef721c6f99e33674265cb15c8c2ce4efc05c Mon Sep 17 00:00:00 2001 From: Michael Geddes Date: Sun, 11 May 2014 09:34:28 +0800 Subject: [PATCH 07/14] test: Implement mingw abspath_of_dir needed to translate /c/ -> c: for comparison. Signed-off-by: Michael Geddes --- t/test-lib.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/t/test-lib.sh b/t/test-lib.sh index 6fdd9374103aa4..1c899c86a400ca 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -858,6 +858,11 @@ case $(uname -s) in *) builtin test "$@";; esac } + + abspath_of_dir () { + (cd "$1" ; pwd -P | sed 's+^/\([a-z]\)\/+\1:/+') + } + # no POSIX permissions # backslashes in pathspec are converted to '/' # exec does not inherit the PID From 2fa9ecaad498f67d94f425e7681e49c2e5b48465 Mon Sep 17 00:00:00 2001 From: Michael Geddes Date: Sun, 23 Dec 2012 10:23:11 +0800 Subject: [PATCH 08/14] test: Factor out 'check_symlink' for stash tests Required by msysgit while the msys core does not handle reading NTFS Symbolic-Link Reparse Points. Signed-off-by: Michael Geddes --- t/t3903-stash.sh | 12 ++++++------ t/test-lib.sh | 4 ++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh index 5b79b216e2e3bb..18914150e5a13d 100755 --- a/t/t3903-stash.sh +++ b/t/t3903-stash.sh @@ -308,7 +308,7 @@ test_expect_success SYMLINKS 'stash file to symlink' ' test -f file && test bar = "$(cat file)" && git stash apply && - case "$(ls -l file)" in *" file -> file2") :;; *) false;; esac + check_symlink file file2 ' test_expect_success SYMLINKS 'stash file to symlink (stage rm)' ' @@ -319,7 +319,7 @@ test_expect_success SYMLINKS 'stash file to symlink (stage rm)' ' test -f file && test bar = "$(cat file)" && git stash apply && - case "$(ls -l file)" in *" file -> file2") :;; *) false;; esac + check_symlink file file2 ' test_expect_success SYMLINKS 'stash file to symlink (full stage)' ' @@ -331,7 +331,7 @@ test_expect_success SYMLINKS 'stash file to symlink (full stage)' ' test -f file && test bar = "$(cat file)" && git stash apply && - case "$(ls -l file)" in *" file -> file2") :;; *) false;; esac + check_symlink file file2 ' # This test creates a commit with a symlink used for the following tests @@ -347,7 +347,7 @@ test_expect_success 'stash symlink to file' ' test_expect_success SYMLINKS 'this must have re-created the symlink' ' test -h filelink && - case "$(ls -l filelink)" in *" filelink -> file") :;; *) false;; esac + check_symlink filelink file ' test_expect_success 'unstash must re-create the file' ' @@ -365,7 +365,7 @@ test_expect_success 'stash symlink to file (stage rm)' ' test_expect_success SYMLINKS 'this must have re-created the symlink' ' test -h filelink && - case "$(ls -l filelink)" in *" filelink -> file") :;; *) false;; esac + check_symlink filelink file ' test_expect_success 'unstash must re-create the file' ' @@ -384,7 +384,7 @@ test_expect_success 'stash symlink to file (full stage)' ' test_expect_success SYMLINKS 'this must have re-created the symlink' ' test -h filelink && - case "$(ls -l filelink)" in *" filelink -> file") :;; *) false;; esac + check_symlink filelink file ' test_expect_success 'unstash must re-create the file' ' diff --git a/t/test-lib.sh b/t/test-lib.sh index 1c899c86a400ca..79e427c349ed9b 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -798,6 +798,10 @@ abspath_of_dir () { (cd "$1" ; pwd -P) } +check_symlink() { + case "$(ls -l $1)" in *" $1 -> $2") :;; *) false;; esac +} + # Fix some commands on Windows case $(uname -s) in *MINGW*) From 3259ff97c32291def09d5b2548159cc6c08b1561 Mon Sep 17 00:00:00 2001 From: Michael Geddes Date: Sun, 23 Dec 2012 10:24:00 +0800 Subject: [PATCH 09/14] test: Override 'check_symlink' to work with incomplete mingw Signed-off-by: Michael Geddes --- t/test-lib.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/t/test-lib.sh b/t/test-lib.sh index 79e427c349ed9b..72b98ef625bd97 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -867,6 +867,11 @@ case $(uname -s) in (cd "$1" ; pwd -P | sed 's+^/\([a-z]\)\/+\1:/+') } + check_symlink() { + check_symlink_LINK="$(cmd /c "dir $1" | grep \ | sed 's/^.*\[\([^]]*\)\].*$/\1/')" + test "$check_symlink_LINK" = "$2" + } + # no POSIX permissions # backslashes in pathspec are converted to '/' # exec does not inherit the PID From 1919ecfdb90631da47279fe5a275f8a3a7a32b73 Mon Sep 17 00:00:00 2001 From: Michael Geddes Date: Wed, 18 Jul 2012 15:31:24 +0800 Subject: [PATCH 10/14] contrib: make git-new-workdir work with windows symlinks. Required for tests to work. Signed-off-by: Michael Geddes --- contrib/workdir/git-new-workdir | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/contrib/workdir/git-new-workdir b/contrib/workdir/git-new-workdir index 75e8b258177f7f..ae4c75f7045f54 100755 --- a/contrib/workdir/git-new-workdir +++ b/contrib/workdir/git-new-workdir @@ -1,5 +1,52 @@ #!/bin/sh +# Fix some commands on Windows +case $(uname -s) in +*MINGW*) + winpath () { + # pwd -W is the only way of getting msys to convert the path, especially mounted paths like /usr + # since cmd /c only takes a single parameter preventing msys automatic path conversion. + if test "${1:~-1:1}" != "/" ; then + echo "$1" | sed 's+/+\\+g' + elif test -d "$1" ; then + (cd "$1"; pwd -W) | sed 's+/+\\+g' + elif test -d "${1%/*}" ; then + (cd "${1%/*}"; echo "$(pwd -W)/${1##*/}") | sed 's+/+\\+g' + else + echo "$1" | sed -e 's+^/\([a-z]\)/+\1:/+' -e 's+/+\\+g' + fi + } + # git sees Windows-style pwd + pwd () { + builtin pwd -W + } + # use mklink + ln () { + + ln_sym_hard=/H + ln_sym_dir= + if test "$1" = "-s" + then + ln_sym_hard= + shift + fi + pushd $(dirname "$2") 2>&1 > /dev/null + builtin test -d "$1" && ln_sym_dir=/D + popd > /dev/null 2> /dev/null + cmd /c "mklink ${ln_sym_hard}${ln_sym_dir} \"$(winpath "$2")\" \"$(winpath "$1")\">/dev/null " 2>/dev/null + } + + test () { + case "$1" in + -h) + test_file=$(cmd /c "@dir /b/a:l \"$(winpath "${2}")\" 2> nul" ) + builtin test -n "${test_file}" + ;; + *) builtin test "$@";; + esac + } +esac + usage () { echo "usage:" $@ exit 127 @@ -70,6 +117,7 @@ do mkdir -p "$(dirname "$new_workdir/.git/$x")" ;; esac + test -e "$git_dir/$x" || mkdir "$git_dir/$x" ln -s "$git_dir/$x" "$new_workdir/.git/$x" done From 0a7779352af7cf83a54db1f3eb0e457744e8f3da Mon Sep 17 00:00:00 2001 From: Michael Geddes Date: Wed, 13 Nov 2013 10:54:15 +0800 Subject: [PATCH 11/14] test: Differentiate ability for gnu utils to handle symlinks from git t7800 failed under symlink enabled msysgit without symlink enabled perl/msys Signed-off-by: Michael Geddes --- t/t7800-difftool.sh | 2 +- t/test-lib.sh | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/t/t7800-difftool.sh b/t/t7800-difftool.sh index 5a193c500d282c..62ac28c6a1a34f 100755 --- a/t/t7800-difftool.sh +++ b/t/t7800-difftool.sh @@ -371,7 +371,7 @@ do done >actual EOF -test_expect_success PERL,SYMLINKS 'difftool --dir-diff --symlink without unstaged changes' ' +test_expect_success PERL,SYMLINKS_SH 'difftool --dir-diff --symlink without unstaged changes' ' cat >expect <<-EOF && file $(pwd)/file diff --git a/t/test-lib.sh b/t/test-lib.sh index 72b98ef625bd97..99e4739637cfca 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -958,6 +958,12 @@ test_lazy_prereq SYMLINKS ' touch x ln -s x y && test -h y ' +test_lazy_prereq SYMLINKS_SH ' + # test whether the filesystem supports symbolic links + touch x + # Then check it is supported by the shell (not using overridden test) + ln -s x y && builtin test -h y +' test_lazy_prereq FILEMODE ' test "$(git config --bool core.filemode)" = true From 1930ec69ec7b64c35a6377c62867b2061fd7b93c Mon Sep 17 00:00:00 2001 From: Michael Geddes Date: Wed, 14 May 2014 08:08:06 +0800 Subject: [PATCH 12/14] test: Fixup 3900 i38n quoting Failure of test_when_finished caused test to fail required for overridden rm () to work without error Signed-off-by: Michael Geddes --- t/t3900-i18n-commit.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/t/t3900-i18n-commit.sh b/t/t3900-i18n-commit.sh index 4bf1dbe9c9f3ff..24e40418fa9120 100755 --- a/t/t3900-i18n-commit.sh +++ b/t/t3900-i18n-commit.sh @@ -40,7 +40,7 @@ test_expect_success 'UTF-16 refused because of NULs' ' ' test_expect_success 'UTF-8 invalid characters refused' ' - test_when_finished "rm -f $HOME/stderr $HOME/invalid" && + test_when_finished "rm -f \"$HOME/stderr\" \"$HOME/invalid\"" && echo "UTF-8 characters" >F && printf "Commit message\n\nInvalid surrogate:\355\240\200\n" \ >"$HOME/invalid" && @@ -49,7 +49,7 @@ test_expect_success 'UTF-8 invalid characters refused' ' ' test_expect_success 'UTF-8 overlong sequences rejected' ' - test_when_finished "rm -f $HOME/stderr $HOME/invalid" && + test_when_finished "rm -f \"$HOME/stderr\" \"$HOME/invalid\"" && rm -f "$HOME/stderr" "$HOME/invalid" && echo "UTF-8 overlong" >F && printf "\340\202\251ommit message\n\nThis is not a space:\300\240\n" \ @@ -59,7 +59,7 @@ test_expect_success 'UTF-8 overlong sequences rejected' ' ' test_expect_success 'UTF-8 non-characters refused' ' - test_when_finished "rm -f $HOME/stderr $HOME/invalid" && + test_when_finished "rm -f \"$HOME/stderr\" \"$HOME/invalid\"" && echo "UTF-8 non-character 1" >F && printf "Commit message\n\nNon-character:\364\217\277\276\n" \ >"$HOME/invalid" && @@ -68,7 +68,7 @@ test_expect_success 'UTF-8 non-characters refused' ' ' test_expect_success 'UTF-8 non-characters refused' ' - test_when_finished "rm -f $HOME/stderr $HOME/invalid" && + test_when_finished "rm -f \"$HOME/stderr\" \"$HOME/invalid\"" && echo "UTF-8 non-character 2." >F && printf "Commit message\n\nNon-character:\357\267\220\n" \ >"$HOME/invalid" && From db19ecf4b0187cc28baf1ef5f6b8bd70b5c69ab1 Mon Sep 17 00:00:00 2001 From: Michael Geddes Date: Thu, 5 Jun 2014 07:11:59 +0800 Subject: [PATCH 13/14] test: Introduce binary compare function In msysgit there were some crashes caused by the line conversions when comparing binary files. I've also used the patch originally by Stepan Kasal to extend coverage of binary compares. Signed-off-by: Michael Geddes --- t/t5000-tar-tree.sh | 34 ++++++++++++++++----------------- t/t5001-archive-attr.sh | 2 +- t/t5003-archive-zip.sh | 6 +++--- t/t5004-archive-corner-cases.sh | 2 +- t/t9300-fast-import.sh | 2 +- t/test-lib-functions.sh | 8 ++++++++ t/test-lib.sh | 1 + 7 files changed, 32 insertions(+), 23 deletions(-) diff --git a/t/t5000-tar-tree.sh b/t/t5000-tar-tree.sh index c325893f754d37..eaf22aa5f9a6da 100755 --- a/t/t5000-tar-tree.sh +++ b/t/t5000-tar-tree.sh @@ -165,7 +165,7 @@ check_tar with_olde-prefix olde- test_expect_success 'git archive on large files' ' test_config core.bigfilethreshold 1 && git archive HEAD >b3.tar && - test_cmp b.tar b3.tar + test_cmp_bin b.tar b3.tar ' test_expect_success \ @@ -174,15 +174,15 @@ test_expect_success \ test_expect_success \ 'git archive vs. the same in a bare repo' \ - 'test_cmp b.tar b3.tar' + 'test_cmp_bin b.tar b3.tar' test_expect_success 'git archive with --output' \ 'git archive --output=b4.tar HEAD && - test_cmp b.tar b4.tar' + test_cmp_bin b.tar b4.tar' test_expect_success 'git archive --remote' \ 'git archive --remote=. HEAD >b5.tar && - test_cmp b.tar b5.tar' + test_cmp_bin b.tar b5.tar' test_expect_success \ 'validate file modification time' \ @@ -199,7 +199,7 @@ test_expect_success \ test_expect_success 'git archive with --output, override inferred format' ' git archive --format=tar --output=d4.zip HEAD && - test_cmp b.tar d4.zip + test_cmp_bin b.tar d4.zip ' test_expect_success \ @@ -245,34 +245,34 @@ test_expect_success 'archive --list shows only enabled remote filters' ' test_expect_success 'invoke tar filter by format' ' git archive --format=tar.foo HEAD >config.tar.foo && tr ab ba config.tar && - test_cmp b.tar config.tar && + test_cmp_bin b.tar config.tar && git archive --format=bar HEAD >config.bar && tr ab ba config.tar && - test_cmp b.tar config.tar + test_cmp_bin b.tar config.tar ' test_expect_success 'invoke tar filter by extension' ' git archive -o config-implicit.tar.foo HEAD && - test_cmp config.tar.foo config-implicit.tar.foo && + test_cmp_bin config.tar.foo config-implicit.tar.foo && git archive -o config-implicit.bar HEAD && - test_cmp config.tar.foo config-implicit.bar + test_cmp_bin config.tar.foo config-implicit.bar ' test_expect_success 'default output format remains tar' ' git archive -o config-implicit.baz HEAD && - test_cmp b.tar config-implicit.baz + test_cmp_bin b.tar config-implicit.baz ' test_expect_success 'extension matching requires dot' ' git archive -o config-implicittar.foo HEAD && - test_cmp b.tar config-implicittar.foo + test_cmp_bin b.tar config-implicittar.foo ' test_expect_success 'only enabled filters are available remotely' ' test_must_fail git archive --remote=. --format=tar.foo HEAD \ >remote.tar.foo && git archive --remote=. --format=bar >remote.bar HEAD && - test_cmp remote.bar config.bar + test_cmp_bin remote.bar config.bar ' test_expect_success GZIP 'git archive --format=tgz' ' @@ -281,27 +281,27 @@ test_expect_success GZIP 'git archive --format=tgz' ' test_expect_success GZIP 'git archive --format=tar.gz' ' git archive --format=tar.gz HEAD >j1.tar.gz && - test_cmp j.tgz j1.tar.gz + test_cmp_bin j.tgz j1.tar.gz ' test_expect_success GZIP 'infer tgz from .tgz filename' ' git archive --output=j2.tgz HEAD && - test_cmp j.tgz j2.tgz + test_cmp_bin j.tgz j2.tgz ' test_expect_success GZIP 'infer tgz from .tar.gz filename' ' git archive --output=j3.tar.gz HEAD && - test_cmp j.tgz j3.tar.gz + test_cmp_bin j.tgz j3.tar.gz ' test_expect_success GZIP 'extract tgz file' ' gzip -d -c j.tar && - test_cmp b.tar j.tar + test_cmp_bin b.tar j.tar ' test_expect_success GZIP 'remote tar.gz is allowed by default' ' git archive --remote=. --format=tar.gz HEAD >remote.tar.gz && - test_cmp j.tgz remote.tar.gz + test_cmp_bin j.tgz remote.tar.gz ' test_expect_success GZIP 'remote tar.gz can be disabled' ' diff --git a/t/t5001-archive-attr.sh b/t/t5001-archive-attr.sh index 51dedab29b6827..b04d955bfa8229 100755 --- a/t/t5001-archive-attr.sh +++ b/t/t5001-archive-attr.sh @@ -68,7 +68,7 @@ test_expect_missing worktree2/ignored-by-worktree test_expect_success 'git archive vs. bare' ' (cd bare && git archive HEAD) >bare-archive.tar && - test_cmp archive.tar bare-archive.tar + test_cmp_bin archive.tar bare-archive.tar ' test_expect_success 'git archive with worktree attributes, bare' ' diff --git a/t/t5003-archive-zip.sh b/t/t5003-archive-zip.sh index c72f71eb18ee90..21a5c93f41e288 100755 --- a/t/t5003-archive-zip.sh +++ b/t/t5003-archive-zip.sh @@ -97,15 +97,15 @@ test_expect_success \ test_expect_success \ 'git archive --format=zip vs. the same in a bare repo' \ - 'test_cmp d.zip d1.zip' + 'test_cmp_bin d.zip d1.zip' test_expect_success 'git archive --format=zip with --output' \ 'git archive --format=zip --output=d2.zip HEAD && - test_cmp d.zip d2.zip' + test_cmp_bin d.zip d2.zip' test_expect_success 'git archive with --output, inferring format' ' git archive --output=d3.zip HEAD && - test_cmp d.zip d3.zip + test_cmp_bin d.zip d3.zip ' test_expect_success \ diff --git a/t/t5004-archive-corner-cases.sh b/t/t5004-archive-corner-cases.sh index 67f3b54bed3545..305bcac6b76511 100755 --- a/t/t5004-archive-corner-cases.sh +++ b/t/t5004-archive-corner-cases.sh @@ -45,7 +45,7 @@ test_expect_success HEADER_ONLY_TAR_OK 'tar archive of commit with empty tree' ' test_expect_success 'tar archive of empty tree is empty' ' git archive --format=tar HEAD: >empty.tar && perl -e "print \"\\0\" x 10240" >10knuls.tar && - test_cmp 10knuls.tar empty.tar + test_cmp_bin 10knuls.tar empty.tar ' test_expect_success 'tar archive of empty tree with prefix' ' diff --git a/t/t9300-fast-import.sh b/t/t9300-fast-import.sh index 27263dfb80420a..2535edc576845d 100755 --- a/t/t9300-fast-import.sh +++ b/t/t9300-fast-import.sh @@ -2687,7 +2687,7 @@ test_expect_success 'R: verify created pack' ' test_expect_success \ 'R: verify written objects' \ 'git --git-dir=R/.git cat-file blob big-file:big1 >actual && - test_cmp expect actual && + test_cmp_bin expect actual && a=$(git --git-dir=R/.git rev-parse big-file:big1) && b=$(git --git-dir=R/.git rev-parse big-file:big2) && test $a = $b' diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh index 158e10a67e5878..8e0c8e23d99b6d 100644 --- a/t/test-lib-functions.sh +++ b/t/test-lib-functions.sh @@ -617,6 +617,14 @@ test_cmp() { $GIT_TEST_CMP "$@" } +test_cmp_bin() { + if test -z "$GIT_TEST_CMP_BIN" ; then + $GIT_TEST_CMP "$@" + else + $GIT_TEST_CMP_BIN "$@" + fi +} + # Check if the file expected to be empty is indeed empty, and barfs # otherwise. diff --git a/t/test-lib.sh b/t/test-lib.sh index 99e4739637cfca..64f1dac1b02718 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -881,6 +881,7 @@ case $(uname -s) in test_set_prereq SED_STRIPS_CR test_set_prereq GREP_STRIPS_CR GIT_TEST_CMP=mingw_test_cmp + GIT_TEST_CMP_BIN="cmp -s -c" ;; *CYGWIN*) test_set_prereq POSIXPERM From 3e5df5d00ca1d5d9be8ad615541d20168ea659bc Mon Sep 17 00:00:00 2001 From: Michael Geddes Date: Sun, 22 Jun 2014 09:48:30 +0800 Subject: [PATCH 14/14] test: Fix remote failure test to use correct env variable The test was using the wrong testgit variable helper to cause the failure. I still don't know why this test wasn't failing on all machines. Signed-off-by: Michael Geddes --- t/t5801-remote-helpers.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/t5801-remote-helpers.sh b/t/t5801-remote-helpers.sh index a00a660763451d..90ae9ef1072aee 100755 --- a/t/t5801-remote-helpers.sh +++ b/t/t5801-remote-helpers.sh @@ -212,7 +212,7 @@ test_expect_success 'push update refs failure' ' echo "update fail" >>file && git commit -a -m "update fail" && git rev-parse --verify testgit/origin/heads/update >expect && - test_expect_code 1 env GIT_REMOTE_TESTGIT_FAILURE="non-fast forward" \ + test_expect_code 1 env GIT_REMOTE_TESTGIT_PUSH_ERROR="non-fast forward" \ git push origin update && git rev-parse --verify testgit/origin/heads/update >actual && test_cmp expect actual