diff --git a/compat/mingw.c b/compat/mingw.c index d87a0e03b6e902..665ff16a646385 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 const char *make_backslash_path(const char *path) +static inline wchar_t *to_backslash_wpath(wchar_t *path) { - static char buf[PATH_MAX + 1]; - char *c; + wchar_t *c; + for (c = path; *c; c++) { + if (*c == '/') + *c = '\\'; + } + return path; +} - if (strlcpy(buf, path, PATH_MAX) >= PATH_MAX) - die("Too long path: %.*s", 60, path); - for (c = buf; *c; c++) { +static inline char *backslash_path(char *path) +{ + char *c; + for (c = path; *c; c++) { if (*c == '/') *c = '\\'; } - return buf; + 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); + + 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; @@ -1897,14 +2268,27 @@ 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); 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 +2307,111 @@ int link(const char *oldpath, const char *newpath) return 0; } +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; + 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; + } + + 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"); + 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, flags)) { + 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..9dbba85937d99c 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) @@ -148,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 */ @@ -164,6 +164,9 @@ 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 mingw_symlink(const char *oldpath, const char *newpath, enum git_target_type targettype); +int readlink(const char *path, char *buf, size_t bufsiz); + /* * replacements of existing functions */ @@ -376,6 +379,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/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 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 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) { 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/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/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/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/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" && 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/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/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..eaf22aa5f9a6da 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 @@ -164,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 \ @@ -173,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' \ @@ -198,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 \ @@ -244,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' ' @@ -280,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/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 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/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 cc74bafa6200f2..64f1dac1b02718 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" @@ -737,6 +794,14 @@ yes () { done } +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*) @@ -754,6 +819,59 @@ 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 + } + + abspath_of_dir () { + (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 @@ -763,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 @@ -837,8 +956,15 @@ test_lazy_prereq PIPE ' test_lazy_prereq SYMLINKS ' # test whether the filesystem supports symbolic links + 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