mirror of
https://github.com/git-for-windows/git.git
synced 2026-05-29 22:43:24 -05:00
Merge branch 'dscho/symlink-attr-extra'
We need to merge this branch early, to avoid merge conflicts in merge commits. Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
This commit is contained in:
@@ -551,6 +551,19 @@ relatively high IO latencies. When enabled, Git will do the
|
||||
index comparison to the filesystem data in parallel, allowing
|
||||
overlapping IO's. Defaults to true.
|
||||
|
||||
core.fscache::
|
||||
Enable additional caching of file system data for some operations.
|
||||
+
|
||||
Git for Windows uses this to bulk-read and cache lstat data of entire
|
||||
directories (instead of doing lstat file by file).
|
||||
|
||||
core.longpaths::
|
||||
Enable long path (> 260) support for builtin commands in Git for
|
||||
Windows. This is disabled by default, as long paths are not supported
|
||||
by Windows Explorer, cmd.exe and the Git for Windows tool chain
|
||||
(msys, bash, tcl, perl...). Only enable this if you know what you're
|
||||
doing and are prepared to live with a few quirks.
|
||||
|
||||
core.unsetenvvars::
|
||||
Windows-only: comma-separated list of environment variables'
|
||||
names that need to be unset before spawning any other process.
|
||||
|
||||
@@ -382,6 +382,36 @@ sign `$` upon checkout. Any byte sequence that begins with
|
||||
with `$Id$` upon check-in.
|
||||
|
||||
|
||||
`symlink`
|
||||
^^^^^^^^^
|
||||
|
||||
On Windows, symbolic links have a type: a "file symlink" must point at
|
||||
a file, and a "directory symlink" must point at a directory. If the
|
||||
type of symlink does not match its target, it doesn't work.
|
||||
|
||||
Git does not record the type of symlink in the index or in a tree. On
|
||||
checkout it'll guess the type, which only works if the target exists
|
||||
at the time the symlink is created. This may often not be the case,
|
||||
for example when the link points at a directory inside a submodule.
|
||||
|
||||
The `symlink` attribute allows you to explicitly set the type of symlink
|
||||
to `file` or `dir`, so Git doesn't have to guess. If you have a set of
|
||||
symlinks that point at other files, you can do:
|
||||
|
||||
------------------------
|
||||
*.gif symlink=file
|
||||
------------------------
|
||||
|
||||
To tell Git that a symlink points at a directory, use:
|
||||
|
||||
------------------------
|
||||
tools_folder symlink=dir
|
||||
------------------------
|
||||
|
||||
The `symlink` attribute is ignored on platforms other than Windows,
|
||||
since they don't distinguish between different types of symlinks.
|
||||
|
||||
|
||||
`filter`
|
||||
^^^^^^^^
|
||||
|
||||
|
||||
2
apply.c
2
apply.c
@@ -4356,7 +4356,7 @@ static int try_create_file(struct apply_state *state, const char *path,
|
||||
/* Although buf:size is counted string, it also is NUL
|
||||
* terminated.
|
||||
*/
|
||||
return !!symlink(buf, path);
|
||||
return !!create_symlink(state && state->repo ? state->repo->index : NULL, buf, path);
|
||||
|
||||
fd = open(path, O_CREAT | O_EXCL | O_WRONLY, (mode & 0100) ? 0777 : 0666);
|
||||
if (fd < 0)
|
||||
|
||||
@@ -579,6 +579,7 @@ static int *list_and_choose(struct menu_opts *opts, struct menu_stuff *stuff)
|
||||
clean_get_color(CLEAN_COLOR_RESET));
|
||||
}
|
||||
|
||||
fflush(stdout);
|
||||
if (strbuf_getline_lf(&choice, stdin) != EOF) {
|
||||
strbuf_trim(&choice);
|
||||
} else {
|
||||
@@ -661,6 +662,7 @@ static int filter_by_patterns_cmd(void)
|
||||
clean_print_color(CLEAN_COLOR_PROMPT);
|
||||
printf(_("Input ignore patterns>> "));
|
||||
clean_print_color(CLEAN_COLOR_RESET);
|
||||
fflush(stdout);
|
||||
if (strbuf_getline_lf(&confirm, stdin) != EOF)
|
||||
strbuf_trim(&confirm);
|
||||
else
|
||||
@@ -759,6 +761,7 @@ static int ask_each_cmd(void)
|
||||
qname = quote_path_relative(item->string, NULL, &buf);
|
||||
/* TRANSLATORS: Make sure to keep [y/N] as is */
|
||||
printf(_("Remove %s [y/N]? "), qname);
|
||||
fflush(stdout);
|
||||
if (strbuf_getline_lf(&confirm, stdin) != EOF) {
|
||||
strbuf_trim(&confirm);
|
||||
} else {
|
||||
|
||||
@@ -1371,6 +1371,7 @@ int cmd_status(int argc, const char **argv, const char *prefix)
|
||||
PATHSPEC_PREFER_FULL,
|
||||
prefix, argv);
|
||||
|
||||
enable_fscache(1);
|
||||
if (status_format != STATUS_FORMAT_PORCELAIN &&
|
||||
status_format != STATUS_FORMAT_PORCELAIN_V2)
|
||||
progress_flag = REFRESH_PROGRESS;
|
||||
|
||||
@@ -505,7 +505,7 @@ static int run_dir_diff(const char *extcmd, int symlinks, const char *prefix,
|
||||
}
|
||||
add_path(&wtdir, wtdir_len, dst_path);
|
||||
if (symlinks) {
|
||||
if (symlink(wtdir.buf, rdir.buf)) {
|
||||
if (create_symlink(lstate.istate, wtdir.buf, rdir.buf)) {
|
||||
ret = error_errno("could not symlink '%s' to '%s'", wtdir.buf, rdir.buf);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
|
||||
if (strbuf_readlink(&lnk, template_path->buf,
|
||||
st_template.st_size) < 0)
|
||||
die_errno(_("cannot readlink '%s'"), template_path->buf);
|
||||
if (symlink(lnk.buf, path->buf))
|
||||
if (create_symlink(NULL, lnk.buf, path->buf))
|
||||
die_errno(_("cannot symlink '%s' '%s'"),
|
||||
lnk.buf, path->buf);
|
||||
strbuf_release(&lnk);
|
||||
@@ -278,7 +278,7 @@ static int create_default_files(const char *template_path,
|
||||
path = git_path_buf(&buf, "tXXXXXX");
|
||||
if (!close(xmkstemp(path)) &&
|
||||
!unlink(path) &&
|
||||
!symlink("testing", path) &&
|
||||
!create_symlink(NULL, "testing", path) &&
|
||||
!lstat(path, &st1) &&
|
||||
S_ISLNK(st1.st_mode))
|
||||
unlink(path); /* good */
|
||||
|
||||
850
compat/mingw.c
850
compat/mingw.c
File diff suppressed because it is too large
Load Diff
105
compat/mingw.h
105
compat/mingw.h
@@ -11,6 +11,9 @@ typedef _sigset_t sigset_t;
|
||||
#undef _POSIX_THREAD_SAFE_FUNCTIONS
|
||||
#endif
|
||||
|
||||
extern int core_fscache;
|
||||
extern int core_long_paths;
|
||||
|
||||
extern int mingw_core_config(const char *var, const char *value, void *cb);
|
||||
#define platform_core_config mingw_core_config
|
||||
|
||||
@@ -120,10 +123,6 @@ struct utsname {
|
||||
* 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; }
|
||||
#ifndef __MINGW64_VERSION_MAJOR
|
||||
@@ -215,6 +214,10 @@ int setitimer(int type, struct itimerval *in, struct itimerval *out);
|
||||
int sigaction(int sig, struct sigaction *in, struct sigaction *out);
|
||||
int link(const char *oldpath, const char *newpath);
|
||||
int uname(struct utsname *buf);
|
||||
int readlink(const char *path, char *buf, size_t bufsiz);
|
||||
struct index_state;
|
||||
int mingw_create_symlink(struct index_state *index, const char *target, const char *link);
|
||||
#define create_symlink mingw_create_symlink
|
||||
|
||||
/*
|
||||
* replacements of existing functions
|
||||
@@ -345,6 +348,17 @@ static inline int getrlimit(int resource, struct rlimit *rlp)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* The unit of FILETIME is 100-nanoseconds since January 1, 1601, UTC.
|
||||
* Returns the 100-nanoseconds ("hekto nanoseconds") since the epoch.
|
||||
*/
|
||||
static inline long long filetime_to_hnsec(const FILETIME *ft)
|
||||
{
|
||||
long long winTime = ((long long)ft->dwHighDateTime << 32) + ft->dwLowDateTime;
|
||||
/* Windows to Unix Epoch conversion */
|
||||
return winTime - 116444736000000000LL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Use mingw specific stat()/lstat()/fstat() implementations on Windows,
|
||||
* including our own struct stat with 64 bit st_size and nanosecond-precision
|
||||
@@ -361,6 +375,13 @@ struct timespec {
|
||||
#endif
|
||||
#endif
|
||||
|
||||
static inline void filetime_to_timespec(const FILETIME *ft, struct timespec *ts)
|
||||
{
|
||||
long long hnsec = filetime_to_hnsec(ft);
|
||||
ts->tv_sec = (time_t)(hnsec / 10000000);
|
||||
ts->tv_nsec = (hnsec % 10000000) * 100;
|
||||
}
|
||||
|
||||
struct mingw_stat {
|
||||
_dev_t st_dev;
|
||||
_ino_t st_ino;
|
||||
@@ -393,7 +414,7 @@ int mingw_fstat(int fd, struct stat *buf);
|
||||
#ifdef lstat
|
||||
#undef lstat
|
||||
#endif
|
||||
#define lstat mingw_lstat
|
||||
extern int (*lstat)(const char *file_name, struct stat *buf);
|
||||
|
||||
|
||||
int mingw_utime(const char *file_name, const struct utimbuf *times);
|
||||
@@ -455,6 +476,42 @@ extern const char *program_data_config(void);
|
||||
#include <inttypes.h>
|
||||
#endif
|
||||
|
||||
/**
|
||||
* 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
|
||||
* value to limit required stack memory.
|
||||
*/
|
||||
#define MAX_LONG_PATH 4096
|
||||
|
||||
/**
|
||||
* Handles paths that would exceed the MAX_PATH limit of Windows Unicode APIs.
|
||||
*
|
||||
* With expand == false, the function checks for over-long paths and fails
|
||||
* with ENAMETOOLONG. The path parameter is not modified, except if cwd + path
|
||||
* exceeds max_path, but the resulting absolute path doesn't (e.g. due to
|
||||
* eliminating '..' components). The path parameter must point to a buffer
|
||||
* of max_path wide characters.
|
||||
*
|
||||
* With expand == true, an over-long path is automatically converted in place
|
||||
* to an absolute path prefixed with '\\?\', and the new length is returned.
|
||||
* The path parameter must point to a buffer of MAX_LONG_PATH wide characters.
|
||||
*
|
||||
* Parameters:
|
||||
* path: path to check and / or convert
|
||||
* len: size of path on input (number of wide chars without \0)
|
||||
* max_path: max short path length to check (usually MAX_PATH = 260, but just
|
||||
* 248 for CreateDirectoryW)
|
||||
* expand: false to only check the length, true to expand the path to a
|
||||
* '\\?\'-prefixed absolute path
|
||||
*
|
||||
* Return:
|
||||
* length of the resulting path, or -1 on failure
|
||||
*
|
||||
* Errors:
|
||||
* ENAMETOOLONG if path is too long
|
||||
*/
|
||||
int handle_long_path(wchar_t *path, int len, int max_path, int expand);
|
||||
|
||||
/**
|
||||
* Converts UTF-8 encoded string to UTF-16LE.
|
||||
*
|
||||
@@ -512,17 +569,45 @@ static inline int xutftowcs(wchar_t *wcs, const char *utf, size_t wcslen)
|
||||
return xutftowcsn(wcs, utf, wcslen, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified file system specific wrapper of xutftowcsn and handle_long_path.
|
||||
* Converts ERANGE to ENAMETOOLONG. If expand is true, wcs must be at least
|
||||
* MAX_LONG_PATH wide chars (see handle_long_path).
|
||||
*/
|
||||
static inline int xutftowcs_path_ex(wchar_t *wcs, const char *utf,
|
||||
size_t wcslen, int utflen, int max_path, int expand)
|
||||
{
|
||||
int result = xutftowcsn(wcs, utf, wcslen, utflen);
|
||||
if (result < 0 && errno == ERANGE)
|
||||
errno = ENAMETOOLONG;
|
||||
if (result >= 0)
|
||||
result = handle_long_path(wcs, result, max_path, expand);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified file system specific variant of xutftowcsn, assumes output
|
||||
* buffer size is MAX_PATH wide chars and input string is \0-terminated,
|
||||
* fails with ENAMETOOLONG if input string is too long.
|
||||
* fails with ENAMETOOLONG if input string is too long. Typically used for
|
||||
* Windows APIs that don't support long paths, e.g. SetCurrentDirectory,
|
||||
* LoadLibrary, CreateProcess...
|
||||
*/
|
||||
static inline int xutftowcs_path(wchar_t *wcs, const char *utf)
|
||||
{
|
||||
int result = xutftowcsn(wcs, utf, MAX_PATH, -1);
|
||||
if (result < 0 && errno == ERANGE)
|
||||
errno = ENAMETOOLONG;
|
||||
return result;
|
||||
return xutftowcs_path_ex(wcs, utf, MAX_PATH, -1, MAX_PATH, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified file system specific variant of xutftowcsn for Windows APIs
|
||||
* that support long paths via '\\?\'-prefix, assumes output buffer size is
|
||||
* MAX_LONG_PATH wide chars, fails with ENAMETOOLONG if input string is too
|
||||
* long. The 'core.longpaths' git-config option controls whether the path
|
||||
* is only checked or expanded to a long path.
|
||||
*/
|
||||
static inline int xutftowcs_long_path(wchar_t *wcs, const char *utf)
|
||||
{
|
||||
return xutftowcs_path_ex(wcs, utf, MAX_LONG_PATH, -1, MAX_PATH,
|
||||
core_long_paths);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "git-compat-util.h"
|
||||
#include "cache.h"
|
||||
#include "compat/terminal.h"
|
||||
#include "sigchain.h"
|
||||
#include "strbuf.h"
|
||||
@@ -194,6 +194,55 @@ static int mingw_getchar(void)
|
||||
}
|
||||
#define getchar mingw_getchar
|
||||
|
||||
static char *shell_prompt(const char *prompt, int echo)
|
||||
{
|
||||
const char *read_input[] = {
|
||||
/* Note: call 'bash' explicitly, as 'read -s' is bash-specific */
|
||||
"bash", "-c", echo ?
|
||||
"cat >/dev/tty && read -r line </dev/tty && echo \"$line\"" :
|
||||
"cat >/dev/tty && read -r -s line </dev/tty && echo \"$line\" && echo >/dev/tty",
|
||||
NULL
|
||||
};
|
||||
struct child_process child = CHILD_PROCESS_INIT;
|
||||
static struct strbuf buffer = STRBUF_INIT;
|
||||
int prompt_len = strlen(prompt), len = -1, code;
|
||||
|
||||
child.argv = read_input;
|
||||
child.in = -1;
|
||||
child.out = -1;
|
||||
child.silent_exec_failure = 1;
|
||||
|
||||
if (start_command(&child))
|
||||
return NULL;
|
||||
|
||||
if (write_in_full(child.in, prompt, prompt_len) != prompt_len) {
|
||||
error("could not write to prompt script");
|
||||
close(child.in);
|
||||
goto ret;
|
||||
}
|
||||
close(child.in);
|
||||
|
||||
strbuf_reset(&buffer);
|
||||
len = strbuf_read(&buffer, child.out, 1024);
|
||||
if (len < 0) {
|
||||
error("could not read from prompt script");
|
||||
goto ret;
|
||||
}
|
||||
|
||||
strbuf_strip_suffix(&buffer, "\n");
|
||||
strbuf_strip_suffix(&buffer, "\r");
|
||||
|
||||
ret:
|
||||
close(child.out);
|
||||
code = finish_command(&child);
|
||||
if (code) {
|
||||
error("failed to execute prompt script (exit code %d)", code);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return len < 0 ? NULL : buffer.buf;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#ifndef FORCE_TEXT
|
||||
@@ -206,6 +255,15 @@ char *git_terminal_prompt(const char *prompt, int echo)
|
||||
int r;
|
||||
FILE *input_fh, *output_fh;
|
||||
|
||||
#ifdef GIT_WINDOWS_NATIVE
|
||||
|
||||
/* try shell_prompt first, fall back to CONIN/OUT if bash is missing */
|
||||
char *result = shell_prompt(prompt, echo);
|
||||
if (result || errno != ENOENT)
|
||||
return result;
|
||||
|
||||
#endif
|
||||
|
||||
input_fh = fopen(INPUT_PATH, "r" FORCE_TEXT);
|
||||
if (!input_fh)
|
||||
return NULL;
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
static inline int file_attr_to_st_mode (DWORD attr)
|
||||
static inline int file_attr_to_st_mode (DWORD attr, DWORD tag)
|
||||
{
|
||||
int fMode = S_IREAD;
|
||||
if (attr & FILE_ATTRIBUTE_DIRECTORY)
|
||||
if ((attr & FILE_ATTRIBUTE_REPARSE_POINT) && tag == IO_REPARSE_TAG_SYMLINK)
|
||||
fMode |= S_IFLNK;
|
||||
else if (attr & FILE_ATTRIBUTE_DIRECTORY)
|
||||
fMode |= S_IFDIR;
|
||||
else
|
||||
fMode |= S_IFREG;
|
||||
|
||||
@@ -1,58 +1,31 @@
|
||||
#include "../../git-compat-util.h"
|
||||
|
||||
struct DIR {
|
||||
typedef struct dirent_DIR {
|
||||
struct DIR base_dir; /* extend base struct DIR */
|
||||
struct dirent dd_dir; /* includes d_type */
|
||||
HANDLE dd_handle; /* FindFirstFile handle */
|
||||
int dd_stat; /* 0-based index */
|
||||
};
|
||||
char dd_name[MAX_PATH * 3]; /* file name (* 3 for UTF-8 conversion) */
|
||||
} dirent_DIR;
|
||||
|
||||
DIR *(*opendir)(const char *dirname) = dirent_opendir;
|
||||
|
||||
static inline void finddata2dirent(struct dirent *ent, WIN32_FIND_DATAW *fdata)
|
||||
{
|
||||
/* convert UTF-16 name to UTF-8 */
|
||||
xwcstoutf(ent->d_name, fdata->cFileName, sizeof(ent->d_name));
|
||||
/* convert UTF-16 name to UTF-8 (d_name points to dirent_DIR.dd_name) */
|
||||
xwcstoutf(ent->d_name, fdata->cFileName, MAX_PATH * 3);
|
||||
|
||||
/* Set file type, based on WIN32_FIND_DATA */
|
||||
if (fdata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
|
||||
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;
|
||||
}
|
||||
|
||||
DIR *opendir(const char *name)
|
||||
{
|
||||
wchar_t pattern[MAX_PATH + 2]; /* + 2 for '/' '*' */
|
||||
WIN32_FIND_DATAW fdata;
|
||||
HANDLE h;
|
||||
int len;
|
||||
DIR *dir;
|
||||
|
||||
/* convert name to UTF-16 and check length < MAX_PATH */
|
||||
if ((len = xutftowcs_path(pattern, name)) < 0)
|
||||
return NULL;
|
||||
|
||||
/* append optional '/' and wildcard '*' */
|
||||
if (len && !is_dir_sep(pattern[len - 1]))
|
||||
pattern[len++] = '/';
|
||||
pattern[len++] = '*';
|
||||
pattern[len] = 0;
|
||||
|
||||
/* open find handle */
|
||||
h = FindFirstFileW(pattern, &fdata);
|
||||
if (h == INVALID_HANDLE_VALUE) {
|
||||
DWORD err = GetLastError();
|
||||
errno = (err == ERROR_DIRECTORY) ? ENOTDIR : err_win_to_posix(err);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* initialize DIR structure and copy first dir entry */
|
||||
dir = xmalloc(sizeof(DIR));
|
||||
dir->dd_handle = h;
|
||||
dir->dd_stat = 0;
|
||||
finddata2dirent(&dir->dd_dir, &fdata);
|
||||
return dir;
|
||||
}
|
||||
|
||||
struct dirent *readdir(DIR *dir)
|
||||
static struct dirent *dirent_readdir(dirent_DIR *dir)
|
||||
{
|
||||
if (!dir) {
|
||||
errno = EBADF; /* No set_errno for mingw */
|
||||
@@ -79,7 +52,7 @@ struct dirent *readdir(DIR *dir)
|
||||
return &dir->dd_dir;
|
||||
}
|
||||
|
||||
int closedir(DIR *dir)
|
||||
static int dirent_closedir(dirent_DIR *dir)
|
||||
{
|
||||
if (!dir) {
|
||||
errno = EBADF;
|
||||
@@ -90,3 +63,44 @@ int closedir(DIR *dir)
|
||||
free(dir);
|
||||
return 0;
|
||||
}
|
||||
|
||||
DIR *dirent_opendir(const char *name)
|
||||
{
|
||||
wchar_t pattern[MAX_LONG_PATH + 2]; /* + 2 for "\*" */
|
||||
WIN32_FIND_DATAW fdata;
|
||||
HANDLE h;
|
||||
int len;
|
||||
dirent_DIR *dir;
|
||||
|
||||
/* convert name to UTF-16 and check length */
|
||||
if ((len = xutftowcs_path_ex(pattern, name, MAX_LONG_PATH, -1,
|
||||
MAX_PATH - 2, core_long_paths)) < 0)
|
||||
return NULL;
|
||||
|
||||
/*
|
||||
* append optional '\' and wildcard '*'. Note: we need to use '\' as
|
||||
* Windows doesn't translate '/' to '\' for "\\?\"-prefixed paths.
|
||||
*/
|
||||
if (len && !is_dir_sep(pattern[len - 1]))
|
||||
pattern[len++] = '\\';
|
||||
pattern[len++] = '*';
|
||||
pattern[len] = 0;
|
||||
|
||||
/* open find handle */
|
||||
h = FindFirstFileW(pattern, &fdata);
|
||||
if (h == INVALID_HANDLE_VALUE) {
|
||||
DWORD err = GetLastError();
|
||||
errno = (err == ERROR_DIRECTORY) ? ENOTDIR : err_win_to_posix(err);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* initialize DIR structure and copy first dir entry */
|
||||
dir = xmalloc(sizeof(dirent_DIR));
|
||||
dir->base_dir.preaddir = (struct dirent *(*)(DIR *dir)) dirent_readdir;
|
||||
dir->base_dir.pclosedir = (int (*)(DIR *dir)) dirent_closedir;
|
||||
dir->dd_dir.d_name = dir->dd_name;
|
||||
dir->dd_handle = h;
|
||||
dir->dd_stat = 0;
|
||||
finddata2dirent(&dir->dd_dir, &fdata);
|
||||
return (DIR*) dir;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
#ifndef DIRENT_H
|
||||
#define DIRENT_H
|
||||
|
||||
typedef struct DIR DIR;
|
||||
|
||||
#define DT_UNKNOWN 0
|
||||
#define DT_DIR 1
|
||||
#define DT_REG 2
|
||||
#define DT_LNK 3
|
||||
|
||||
struct dirent {
|
||||
unsigned char d_type; /* file type to prevent lstat after readdir */
|
||||
char d_name[MAX_PATH * 3]; /* file name (* 3 for UTF-8 conversion) */
|
||||
unsigned char d_type; /* file type to prevent lstat after readdir */
|
||||
char *d_name; /* file name */
|
||||
};
|
||||
|
||||
DIR *opendir(const char *dirname);
|
||||
struct dirent *readdir(DIR *dir);
|
||||
int closedir(DIR *dir);
|
||||
/*
|
||||
* Base DIR structure, contains pointers to readdir/closedir implementations so
|
||||
* that opendir may choose a concrete implementation on a call-by-call basis.
|
||||
*/
|
||||
typedef struct DIR {
|
||||
struct dirent *(*preaddir)(struct DIR *dir);
|
||||
int (*pclosedir)(struct DIR *dir);
|
||||
} DIR;
|
||||
|
||||
/* default dirent implementation */
|
||||
extern DIR *dirent_opendir(const char *dirname);
|
||||
|
||||
/* current dirent implementation */
|
||||
extern DIR *(*opendir)(const char *dirname);
|
||||
|
||||
#define readdir(dir) (dir->preaddir(dir))
|
||||
#define closedir(dir) (dir->pclosedir(dir))
|
||||
|
||||
#endif /* DIRENT_H */
|
||||
|
||||
494
compat/win32/fscache.c
Normal file
494
compat/win32/fscache.c
Normal file
@@ -0,0 +1,494 @@
|
||||
#include "../../cache.h"
|
||||
#include "../../hashmap.h"
|
||||
#include "../win32.h"
|
||||
#include "fscache.h"
|
||||
|
||||
static int initialized;
|
||||
static volatile long enabled;
|
||||
static struct hashmap map;
|
||||
static CRITICAL_SECTION mutex;
|
||||
|
||||
/*
|
||||
* An entry in the file system cache. Used for both entire directory listings
|
||||
* and file entries.
|
||||
*/
|
||||
struct fsentry {
|
||||
struct hashmap_entry ent;
|
||||
mode_t st_mode;
|
||||
/* Length of name. */
|
||||
unsigned short len;
|
||||
/*
|
||||
* Name of the entry. For directory listings: relative path of the
|
||||
* directory, without trailing '/' (empty for cwd()). For file entries:
|
||||
* name of the file. Typically points to the end of the structure if
|
||||
* the fsentry is allocated on the heap (see fsentry_alloc), or to a
|
||||
* local variable if on the stack (see fsentry_init).
|
||||
*/
|
||||
const char *name;
|
||||
/* Pointer to the directory listing, or NULL for the listing itself. */
|
||||
struct fsentry *list;
|
||||
/* Pointer to the next file entry of the list. */
|
||||
struct fsentry *next;
|
||||
|
||||
union {
|
||||
/* Reference count of the directory listing. */
|
||||
volatile long refcnt;
|
||||
/* Handle to wait on the loading thread. */
|
||||
HANDLE hwait;
|
||||
struct {
|
||||
/* More stat members (only used for file entries). */
|
||||
off64_t st_size;
|
||||
struct timespec st_atim;
|
||||
struct timespec st_mtim;
|
||||
struct timespec st_ctim;
|
||||
} s;
|
||||
} u;
|
||||
};
|
||||
|
||||
/*
|
||||
* Compares the paths of two fsentry structures for equality.
|
||||
*/
|
||||
static int fsentry_cmp(void *unused_cmp_data,
|
||||
const struct fsentry *fse1, const struct fsentry *fse2,
|
||||
void *unused_keydata)
|
||||
{
|
||||
int res;
|
||||
if (fse1 == fse2)
|
||||
return 0;
|
||||
|
||||
/* compare the list parts first */
|
||||
if (fse1->list != fse2->list &&
|
||||
(res = fsentry_cmp(NULL, fse1->list ? fse1->list : fse1,
|
||||
fse2->list ? fse2->list : fse2, NULL)))
|
||||
return res;
|
||||
|
||||
/* if list parts are equal, compare len and name */
|
||||
if (fse1->len != fse2->len)
|
||||
return fse1->len - fse2->len;
|
||||
return strnicmp(fse1->name, fse2->name, fse1->len);
|
||||
}
|
||||
|
||||
/*
|
||||
* Calculates the hash code of an fsentry structure's path.
|
||||
*/
|
||||
static unsigned int fsentry_hash(const struct fsentry *fse)
|
||||
{
|
||||
unsigned int hash = fse->list ? fse->list->ent.hash : 0;
|
||||
return hash ^ memihash(fse->name, fse->len);
|
||||
}
|
||||
|
||||
/*
|
||||
* Initialize an fsentry structure for use by fsentry_hash and fsentry_cmp.
|
||||
*/
|
||||
static void fsentry_init(struct fsentry *fse, struct fsentry *list,
|
||||
const char *name, size_t len)
|
||||
{
|
||||
fse->list = list;
|
||||
fse->name = name;
|
||||
fse->len = len;
|
||||
hashmap_entry_init(fse, fsentry_hash(fse));
|
||||
}
|
||||
|
||||
/*
|
||||
* Allocate an fsentry structure on the heap.
|
||||
*/
|
||||
static struct fsentry *fsentry_alloc(struct fsentry *list, const char *name,
|
||||
size_t len)
|
||||
{
|
||||
/* overallocate fsentry and copy the name to the end */
|
||||
struct fsentry *fse = xmalloc(sizeof(struct fsentry) + len + 1);
|
||||
char *nm = ((char*) fse) + sizeof(struct fsentry);
|
||||
memcpy(nm, name, len);
|
||||
nm[len] = 0;
|
||||
/* init the rest of the structure */
|
||||
fsentry_init(fse, list, nm, len);
|
||||
fse->next = NULL;
|
||||
fse->u.refcnt = 1;
|
||||
return fse;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add a reference to an fsentry.
|
||||
*/
|
||||
inline static void fsentry_addref(struct fsentry *fse)
|
||||
{
|
||||
if (fse->list)
|
||||
fse = fse->list;
|
||||
|
||||
InterlockedIncrement(&(fse->u.refcnt));
|
||||
}
|
||||
|
||||
/*
|
||||
* Release the reference to an fsentry, frees the memory if its the last ref.
|
||||
*/
|
||||
static void fsentry_release(struct fsentry *fse)
|
||||
{
|
||||
if (fse->list)
|
||||
fse = fse->list;
|
||||
|
||||
if (InterlockedDecrement(&(fse->u.refcnt)))
|
||||
return;
|
||||
|
||||
while (fse) {
|
||||
struct fsentry *next = fse->next;
|
||||
free(fse);
|
||||
fse = next;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Allocate and initialize an fsentry from a WIN32_FIND_DATA structure.
|
||||
*/
|
||||
static struct fsentry *fseentry_create_entry(struct fsentry *list,
|
||||
const WIN32_FIND_DATAW *fdata)
|
||||
{
|
||||
char buf[MAX_PATH * 3];
|
||||
int len;
|
||||
struct fsentry *fse;
|
||||
len = xwcstoutf(buf, fdata->cFileName, ARRAY_SIZE(buf));
|
||||
|
||||
fse = fsentry_alloc(list, buf, len);
|
||||
|
||||
fse->st_mode = file_attr_to_st_mode(fdata->dwFileAttributes,
|
||||
fdata->dwReserved0);
|
||||
fse->u.s.st_size = S_ISLNK(fse->st_mode) ? MAX_LONG_PATH :
|
||||
fdata->nFileSizeLow | (((off_t) fdata->nFileSizeHigh) << 32);
|
||||
filetime_to_timespec(&(fdata->ftLastAccessTime), &(fse->u.s.st_atim));
|
||||
filetime_to_timespec(&(fdata->ftLastWriteTime), &(fse->u.s.st_mtim));
|
||||
filetime_to_timespec(&(fdata->ftCreationTime), &(fse->u.s.st_ctim));
|
||||
|
||||
return fse;
|
||||
}
|
||||
|
||||
/*
|
||||
* Create an fsentry-based directory listing (similar to opendir / readdir).
|
||||
* Dir should not contain trailing '/'. Use an empty string for the current
|
||||
* directory (not "."!).
|
||||
*/
|
||||
static struct fsentry *fsentry_create_list(const struct fsentry *dir)
|
||||
{
|
||||
wchar_t pattern[MAX_LONG_PATH + 2]; /* + 2 for "\*" */
|
||||
WIN32_FIND_DATAW fdata;
|
||||
HANDLE h;
|
||||
int wlen;
|
||||
struct fsentry *list, **phead;
|
||||
DWORD err;
|
||||
|
||||
/* convert name to UTF-16 and check length */
|
||||
if ((wlen = xutftowcs_path_ex(pattern, dir->name, MAX_LONG_PATH,
|
||||
dir->len, MAX_PATH - 2, core_long_paths)) < 0)
|
||||
return NULL;
|
||||
|
||||
/*
|
||||
* append optional '\' and wildcard '*'. Note: we need to use '\' as
|
||||
* Windows doesn't translate '/' to '\' for "\\?\"-prefixed paths.
|
||||
*/
|
||||
if (wlen)
|
||||
pattern[wlen++] = '\\';
|
||||
pattern[wlen++] = '*';
|
||||
pattern[wlen] = 0;
|
||||
|
||||
/* open find handle */
|
||||
h = FindFirstFileW(pattern, &fdata);
|
||||
if (h == INVALID_HANDLE_VALUE) {
|
||||
err = GetLastError();
|
||||
errno = (err == ERROR_DIRECTORY) ? ENOTDIR : err_win_to_posix(err);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* allocate object to hold directory listing */
|
||||
list = fsentry_alloc(NULL, dir->name, dir->len);
|
||||
|
||||
/* walk directory and build linked list of fsentry structures */
|
||||
phead = &list->next;
|
||||
do {
|
||||
*phead = fseentry_create_entry(list, &fdata);
|
||||
phead = &(*phead)->next;
|
||||
} while (FindNextFileW(h, &fdata));
|
||||
|
||||
/* remember result of last FindNextFile, then close find handle */
|
||||
err = GetLastError();
|
||||
FindClose(h);
|
||||
|
||||
/* return the list if we've got all the files */
|
||||
if (err == ERROR_NO_MORE_FILES)
|
||||
return list;
|
||||
|
||||
/* otherwise free the list and return error */
|
||||
fsentry_release(list);
|
||||
errno = err_win_to_posix(err);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Adds a directory listing to the cache.
|
||||
*/
|
||||
static void fscache_add(struct fsentry *fse)
|
||||
{
|
||||
if (fse->list)
|
||||
fse = fse->list;
|
||||
|
||||
for (; fse; fse = fse->next)
|
||||
hashmap_add(&map, fse);
|
||||
}
|
||||
|
||||
/*
|
||||
* Clears the cache.
|
||||
*/
|
||||
static void fscache_clear(void)
|
||||
{
|
||||
hashmap_free(&map, 1);
|
||||
hashmap_init(&map, (hashmap_cmp_fn)fsentry_cmp, NULL, 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks if the cache is enabled for the given path.
|
||||
*/
|
||||
static inline int fscache_enabled(const char *path)
|
||||
{
|
||||
return enabled > 0 && !is_absolute_path(path);
|
||||
}
|
||||
|
||||
/*
|
||||
* Looks up a cache entry, waits if its being loaded by another thread.
|
||||
* The mutex must be owned by the calling thread.
|
||||
*/
|
||||
static struct fsentry *fscache_get_wait(struct fsentry *key)
|
||||
{
|
||||
struct fsentry *fse = hashmap_get(&map, key, NULL);
|
||||
|
||||
/* return if its a 'real' entry (future entries have refcnt == 0) */
|
||||
if (!fse || fse->list || fse->u.refcnt)
|
||||
return fse;
|
||||
|
||||
/* create an event and link our key to the future entry */
|
||||
key->u.hwait = CreateEvent(NULL, TRUE, FALSE, NULL);
|
||||
key->next = fse->next;
|
||||
fse->next = key;
|
||||
|
||||
/* wait for the loading thread to signal us */
|
||||
LeaveCriticalSection(&mutex);
|
||||
WaitForSingleObject(key->u.hwait, INFINITE);
|
||||
CloseHandle(key->u.hwait);
|
||||
EnterCriticalSection(&mutex);
|
||||
|
||||
/* repeat cache lookup */
|
||||
return hashmap_get(&map, key, NULL);
|
||||
}
|
||||
|
||||
/*
|
||||
* Looks up or creates a cache entry for the specified key.
|
||||
*/
|
||||
static struct fsentry *fscache_get(struct fsentry *key)
|
||||
{
|
||||
struct fsentry *fse, *future, *waiter;
|
||||
|
||||
EnterCriticalSection(&mutex);
|
||||
/* check if entry is in cache */
|
||||
fse = fscache_get_wait(key);
|
||||
if (fse) {
|
||||
fsentry_addref(fse);
|
||||
LeaveCriticalSection(&mutex);
|
||||
return fse;
|
||||
}
|
||||
/* if looking for a file, check if directory listing is in cache */
|
||||
if (!fse && key->list) {
|
||||
fse = fscache_get_wait(key->list);
|
||||
if (fse) {
|
||||
LeaveCriticalSection(&mutex);
|
||||
/* dir entry without file entry -> file doesn't exist */
|
||||
errno = ENOENT;
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* add future entry to indicate that we're loading it */
|
||||
future = key->list ? key->list : key;
|
||||
future->next = NULL;
|
||||
future->u.refcnt = 0;
|
||||
hashmap_add(&map, future);
|
||||
|
||||
/* create the directory listing (outside mutex!) */
|
||||
LeaveCriticalSection(&mutex);
|
||||
fse = fsentry_create_list(future);
|
||||
EnterCriticalSection(&mutex);
|
||||
|
||||
/* remove future entry and signal waiting threads */
|
||||
hashmap_remove(&map, future, NULL);
|
||||
waiter = future->next;
|
||||
while (waiter) {
|
||||
HANDLE h = waiter->u.hwait;
|
||||
waiter = waiter->next;
|
||||
SetEvent(h);
|
||||
}
|
||||
|
||||
/* leave on error (errno set by fsentry_create_list) */
|
||||
if (!fse) {
|
||||
LeaveCriticalSection(&mutex);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* add directory listing to the cache */
|
||||
fscache_add(fse);
|
||||
|
||||
/* lookup file entry if requested (fse already points to directory) */
|
||||
if (key->list)
|
||||
fse = hashmap_get(&map, key, NULL);
|
||||
|
||||
/* return entry or ENOENT */
|
||||
if (fse)
|
||||
fsentry_addref(fse);
|
||||
else
|
||||
errno = ENOENT;
|
||||
|
||||
LeaveCriticalSection(&mutex);
|
||||
return fse;
|
||||
}
|
||||
|
||||
/*
|
||||
* Enables or disables the cache. Note that the cache is read-only, changes to
|
||||
* the working directory are NOT reflected in the cache while enabled.
|
||||
*/
|
||||
int fscache_enable(int enable)
|
||||
{
|
||||
int result;
|
||||
|
||||
if (!initialized) {
|
||||
/* allow the cache to be disabled entirely */
|
||||
if (!core_fscache)
|
||||
return 0;
|
||||
|
||||
InitializeCriticalSection(&mutex);
|
||||
hashmap_init(&map, (hashmap_cmp_fn) fsentry_cmp, NULL, 0);
|
||||
initialized = 1;
|
||||
}
|
||||
|
||||
result = enable ? InterlockedIncrement(&enabled)
|
||||
: InterlockedDecrement(&enabled);
|
||||
|
||||
if (enable && result == 1) {
|
||||
/* redirect opendir and lstat to the fscache implementations */
|
||||
opendir = fscache_opendir;
|
||||
lstat = fscache_lstat;
|
||||
} else if (!enable && !result) {
|
||||
/* reset opendir and lstat to the original implementations */
|
||||
opendir = dirent_opendir;
|
||||
lstat = mingw_lstat;
|
||||
EnterCriticalSection(&mutex);
|
||||
fscache_clear();
|
||||
LeaveCriticalSection(&mutex);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* Lstat replacement, uses the cache if enabled, otherwise redirects to
|
||||
* mingw_lstat.
|
||||
*/
|
||||
int fscache_lstat(const char *filename, struct stat *st)
|
||||
{
|
||||
int dirlen, base, len;
|
||||
struct fsentry key[2], *fse;
|
||||
|
||||
if (!fscache_enabled(filename))
|
||||
return mingw_lstat(filename, st);
|
||||
|
||||
/* split filename into path + name */
|
||||
len = strlen(filename);
|
||||
if (len && is_dir_sep(filename[len - 1]))
|
||||
len--;
|
||||
base = len;
|
||||
while (base && !is_dir_sep(filename[base - 1]))
|
||||
base--;
|
||||
dirlen = base ? base - 1 : 0;
|
||||
|
||||
/* lookup entry for path + name in cache */
|
||||
fsentry_init(key, NULL, filename, dirlen);
|
||||
fsentry_init(key + 1, key, filename + base, len - base);
|
||||
fse = fscache_get(key + 1);
|
||||
if (!fse)
|
||||
return -1;
|
||||
|
||||
/* copy stat data */
|
||||
st->st_ino = 0;
|
||||
st->st_gid = 0;
|
||||
st->st_uid = 0;
|
||||
st->st_dev = 0;
|
||||
st->st_rdev = 0;
|
||||
st->st_nlink = 1;
|
||||
st->st_mode = fse->st_mode;
|
||||
st->st_size = fse->u.s.st_size;
|
||||
st->st_atim = fse->u.s.st_atim;
|
||||
st->st_mtim = fse->u.s.st_mtim;
|
||||
st->st_ctim = fse->u.s.st_ctim;
|
||||
|
||||
/* don't forget to release fsentry */
|
||||
fsentry_release(fse);
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef struct fscache_DIR {
|
||||
struct DIR base_dir; /* extend base struct DIR */
|
||||
struct fsentry *pfsentry;
|
||||
struct dirent dirent;
|
||||
} fscache_DIR;
|
||||
|
||||
/*
|
||||
* Readdir replacement.
|
||||
*/
|
||||
static struct dirent *fscache_readdir(DIR *base_dir)
|
||||
{
|
||||
fscache_DIR *dir = (fscache_DIR*) base_dir;
|
||||
struct fsentry *next = dir->pfsentry->next;
|
||||
if (!next)
|
||||
return NULL;
|
||||
dir->pfsentry = next;
|
||||
dir->dirent.d_type = S_ISREG(next->st_mode) ? DT_REG :
|
||||
S_ISDIR(next->st_mode) ? DT_DIR : DT_LNK;
|
||||
dir->dirent.d_name = (char*) next->name;
|
||||
return &(dir->dirent);
|
||||
}
|
||||
|
||||
/*
|
||||
* Closedir replacement.
|
||||
*/
|
||||
static int fscache_closedir(DIR *base_dir)
|
||||
{
|
||||
fscache_DIR *dir = (fscache_DIR*) base_dir;
|
||||
fsentry_release(dir->pfsentry);
|
||||
free(dir);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Opendir replacement, uses a directory listing from the cache if enabled,
|
||||
* otherwise calls original dirent implementation.
|
||||
*/
|
||||
DIR *fscache_opendir(const char *dirname)
|
||||
{
|
||||
struct fsentry key, *list;
|
||||
fscache_DIR *dir;
|
||||
int len;
|
||||
|
||||
if (!fscache_enabled(dirname))
|
||||
return dirent_opendir(dirname);
|
||||
|
||||
/* prepare name (strip trailing '/', replace '.') */
|
||||
len = strlen(dirname);
|
||||
if ((len == 1 && dirname[0] == '.') ||
|
||||
(len && is_dir_sep(dirname[len - 1])))
|
||||
len--;
|
||||
|
||||
/* get directory listing from cache */
|
||||
fsentry_init(&key, NULL, dirname, len);
|
||||
list = fscache_get(&key);
|
||||
if (!list)
|
||||
return NULL;
|
||||
|
||||
/* alloc and return DIR structure */
|
||||
dir = (fscache_DIR*) xmalloc(sizeof(fscache_DIR));
|
||||
dir->base_dir.preaddir = fscache_readdir;
|
||||
dir->base_dir.pclosedir = fscache_closedir;
|
||||
dir->pfsentry = list;
|
||||
return (DIR*) dir;
|
||||
}
|
||||
10
compat/win32/fscache.h
Normal file
10
compat/win32/fscache.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#ifndef FSCACHE_H
|
||||
#define FSCACHE_H
|
||||
|
||||
int fscache_enable(int enable);
|
||||
#define enable_fscache(x) fscache_enable(x)
|
||||
|
||||
DIR *fscache_opendir(const char *dir);
|
||||
int fscache_lstat(const char *file_name, struct stat *buf);
|
||||
|
||||
#endif
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <wingdi.h>
|
||||
#include <winreg.h>
|
||||
#include "win32.h"
|
||||
#include "win32/lazyload.h"
|
||||
|
||||
static int fd_is_interactive[3] = { 0, 0, 0 };
|
||||
#define FD_CONSOLE 0x1
|
||||
@@ -41,26 +42,21 @@ typedef struct _CONSOLE_FONT_INFOEX {
|
||||
#endif
|
||||
#endif
|
||||
|
||||
typedef BOOL (WINAPI *PGETCURRENTCONSOLEFONTEX)(HANDLE, BOOL,
|
||||
PCONSOLE_FONT_INFOEX);
|
||||
|
||||
static void warn_if_raster_font(void)
|
||||
{
|
||||
DWORD fontFamily = 0;
|
||||
PGETCURRENTCONSOLEFONTEX pGetCurrentConsoleFontEx;
|
||||
DECLARE_PROC_ADDR(kernel32.dll, BOOL, GetCurrentConsoleFontEx,
|
||||
HANDLE, BOOL, PCONSOLE_FONT_INFOEX);
|
||||
|
||||
/* don't bother if output was ascii only */
|
||||
if (!non_ascii_used)
|
||||
return;
|
||||
|
||||
/* GetCurrentConsoleFontEx is available since Vista */
|
||||
pGetCurrentConsoleFontEx = (PGETCURRENTCONSOLEFONTEX) GetProcAddress(
|
||||
GetModuleHandle("kernel32.dll"),
|
||||
"GetCurrentConsoleFontEx");
|
||||
if (pGetCurrentConsoleFontEx) {
|
||||
if (INIT_PROC_ADDR(GetCurrentConsoleFontEx)) {
|
||||
CONSOLE_FONT_INFOEX cfi;
|
||||
cfi.cbSize = sizeof(cfi);
|
||||
if (pGetCurrentConsoleFontEx(console, 0, &cfi))
|
||||
if (GetCurrentConsoleFontEx(console, 0, &cfi))
|
||||
fontFamily = cfi.FontFamily;
|
||||
} else {
|
||||
/* pre-Vista: check default console font in registry */
|
||||
|
||||
@@ -430,7 +430,7 @@ ifeq ($(uname_S),Windows)
|
||||
COMPAT_OBJS = compat/msvc.o compat/winansi.o \
|
||||
compat/win32/path-utils.o \
|
||||
compat/win32/pthread.o compat/win32/syslog.o \
|
||||
compat/win32/dirent.o
|
||||
compat/win32/dirent.o compat/win32/fscache.o
|
||||
COMPAT_CFLAGS = -D__USE_MINGW_ACCESS -DDETECT_MSYS_TTY -DNOGDI -DHAVE_STRING_H -Icompat -Icompat/regex -Icompat/win32 -DSTRIP_EXTENSION=\".exe\"
|
||||
BASIC_LDFLAGS = -IGNORE:4217 -IGNORE:4049 -NOLOGO -ENTRY:wmainCRTStartup -SUBSYSTEM:CONSOLE
|
||||
# invalidcontinue.obj allows Git's source code to close the same file
|
||||
@@ -609,7 +609,7 @@ ifneq (,$(findstring MINGW,$(uname_S)))
|
||||
COMPAT_OBJS += compat/mingw.o compat/winansi.o \
|
||||
compat/win32/path-utils.o \
|
||||
compat/win32/pthread.o compat/win32/syslog.o \
|
||||
compat/win32/dirent.o
|
||||
compat/win32/dirent.o compat/win32/fscache.o
|
||||
BASIC_CFLAGS += -DWIN32 -DPROTECT_NTFS_DEFAULT=1
|
||||
EXTLIBS += -lws2_32
|
||||
GITLIBS += git.res
|
||||
@@ -664,6 +664,7 @@ else
|
||||
NO_LIBPCRE1_JIT = UnfortunatelyYes
|
||||
NO_CURL =
|
||||
USE_NED_ALLOCATOR = YesPlease
|
||||
NO_PYTHON =
|
||||
else
|
||||
COMPAT_CFLAGS += -D__USE_MINGW_ANSI_STDIO
|
||||
NO_CURL = YesPlease
|
||||
|
||||
2
entry.c
2
entry.c
@@ -289,7 +289,7 @@ static int write_entry(struct cache_entry *ce,
|
||||
if (!has_symlinks || to_tempfile)
|
||||
goto write_file_entry;
|
||||
|
||||
ret = symlink(new_blob, path);
|
||||
ret = create_symlink(state->istate, new_blob, path);
|
||||
free(new_blob);
|
||||
if (ret)
|
||||
return error_errno("unable to create symlink %s", path);
|
||||
|
||||
@@ -208,9 +208,11 @@
|
||||
/* pull in Windows compatibility stuff */
|
||||
#include "compat/win32/path-utils.h"
|
||||
#include "compat/mingw.h"
|
||||
#include "compat/win32/fscache.h"
|
||||
#elif defined(_MSC_VER)
|
||||
#include "compat/win32/path-utils.h"
|
||||
#include "compat/msvc.h"
|
||||
#include "compat/win32/fscache.h"
|
||||
#else
|
||||
#include <sys/utsname.h>
|
||||
#include <sys/wait.h>
|
||||
@@ -404,6 +406,15 @@ static inline char *git_find_last_dir_sep(const char *path)
|
||||
#define find_last_dir_sep git_find_last_dir_sep
|
||||
#endif
|
||||
|
||||
#ifndef create_symlink
|
||||
struct index_state;
|
||||
static inline int git_create_symlink(struct index_state *index, const char *target, const char *link)
|
||||
{
|
||||
return symlink(target, link);
|
||||
}
|
||||
#define create_symlink git_create_symlink
|
||||
#endif
|
||||
|
||||
#ifndef query_user_email
|
||||
#define query_user_email() NULL
|
||||
#endif
|
||||
@@ -1271,6 +1282,21 @@ static inline int is_missing_file_error(int errno_)
|
||||
return (errno_ == ENOENT || errno_ == ENOTDIR);
|
||||
}
|
||||
|
||||
/*
|
||||
* Enable/disable a read-only cache for file system data on platforms that
|
||||
* support it.
|
||||
*
|
||||
* Implementing a live-cache is complicated and requires special platform
|
||||
* support (inotify, ReadDirectoryChangesW...). enable_fscache shall be used
|
||||
* to mark sections of git code that extensively read from the file system
|
||||
* without modifying anything. Implementations can use this to cache e.g. stat
|
||||
* data or even file content without the need to synchronize with the file
|
||||
* system.
|
||||
*/
|
||||
#ifndef enable_fscache
|
||||
#define enable_fscache(x) /* noop */
|
||||
#endif
|
||||
|
||||
extern int cmd_main(int, const char **);
|
||||
|
||||
/*
|
||||
|
||||
@@ -293,11 +293,9 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig
|
||||
struct child_process gpg = CHILD_PROCESS_INIT;
|
||||
int ret;
|
||||
size_t i, j, bottom;
|
||||
struct strbuf gpg_status = STRBUF_INIT;
|
||||
|
||||
argv_array_pushl(&gpg.args,
|
||||
use_format->program,
|
||||
"--status-fd=2",
|
||||
"-bsau", signing_key,
|
||||
NULL);
|
||||
|
||||
@@ -309,12 +307,10 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig
|
||||
*/
|
||||
sigchain_push(SIGPIPE, SIG_IGN);
|
||||
ret = pipe_command(&gpg, buffer->buf, buffer->len,
|
||||
signature, 1024, &gpg_status, 0);
|
||||
signature, 1024, NULL, 0);
|
||||
sigchain_pop(SIGPIPE);
|
||||
|
||||
ret |= !strstr(gpg_status.buf, "\n[GNUPG:] SIG_CREATED ");
|
||||
strbuf_release(&gpg_status);
|
||||
if (ret)
|
||||
if (ret || signature->len == bottom)
|
||||
return error(_("gpg failed to sign the data"));
|
||||
|
||||
/* Strip CR from the line endings, in case we are on Windows. */
|
||||
|
||||
@@ -17,14 +17,14 @@ static void trim_last_path_component(struct strbuf *path)
|
||||
int i = path->len;
|
||||
|
||||
/* back up past trailing slashes, if any */
|
||||
while (i && path->buf[i - 1] == '/')
|
||||
while (i && is_dir_sep(path->buf[i - 1]))
|
||||
i--;
|
||||
|
||||
/*
|
||||
* then go backwards until a slash, or the beginning of the
|
||||
* string
|
||||
*/
|
||||
while (i && path->buf[i - 1] != '/')
|
||||
while (i && !is_dir_sep(path->buf[i - 1]))
|
||||
i--;
|
||||
|
||||
strbuf_setlen(path, i);
|
||||
|
||||
@@ -1011,7 +1011,7 @@ static int update_file_flags(struct merge_options *o,
|
||||
char *lnk = xmemdupz(buf, size);
|
||||
safe_create_leading_directories_const(path);
|
||||
unlink(path);
|
||||
if (symlink(lnk, path))
|
||||
if (create_symlink(&o->orig_index, lnk, path))
|
||||
ret = err(o, _("failed to symlink '%s': %s"),
|
||||
path, strerror(errno));
|
||||
free(lnk);
|
||||
|
||||
@@ -120,6 +120,7 @@ void preload_index(struct index_state *index,
|
||||
pthread_mutex_init(&pd.mutex, NULL);
|
||||
}
|
||||
|
||||
enable_fscache(1);
|
||||
for (i = 0; i < threads; i++) {
|
||||
struct thread_data *p = data+i;
|
||||
int err;
|
||||
@@ -145,6 +146,7 @@ void preload_index(struct index_state *index,
|
||||
stop_progress(&pd.progress);
|
||||
|
||||
trace_performance_leave("preload index");
|
||||
enable_fscache(0);
|
||||
}
|
||||
|
||||
int repo_read_index_preload(struct repository *repo,
|
||||
|
||||
@@ -1786,7 +1786,7 @@ static int create_ref_symlink(struct ref_lock *lock, const char *target)
|
||||
#ifndef NO_SYMLINK_HEAD
|
||||
char *ref_path = get_locked_file_path(&lock->lk);
|
||||
unlink(ref_path);
|
||||
ret = symlink(target, ref_path);
|
||||
ret = create_symlink(NULL, target, ref_path);
|
||||
free(ref_path);
|
||||
|
||||
if (ret)
|
||||
|
||||
10
strbuf.c
10
strbuf.c
@@ -519,8 +519,6 @@ ssize_t strbuf_write(struct strbuf *sb, FILE *f)
|
||||
}
|
||||
|
||||
|
||||
#define STRBUF_MAXLINK (2*PATH_MAX)
|
||||
|
||||
int strbuf_readlink(struct strbuf *sb, const char *path, size_t hint)
|
||||
{
|
||||
size_t oldalloc = sb->alloc;
|
||||
@@ -528,15 +526,15 @@ int strbuf_readlink(struct strbuf *sb, const char *path, size_t hint)
|
||||
if (hint < 32)
|
||||
hint = 32;
|
||||
|
||||
while (hint < STRBUF_MAXLINK) {
|
||||
for (;;) {
|
||||
ssize_t len;
|
||||
|
||||
strbuf_grow(sb, hint);
|
||||
len = readlink(path, sb->buf, hint);
|
||||
strbuf_grow(sb, hint + 1);
|
||||
len = readlink(path, sb->buf, hint + 1);
|
||||
if (len < 0) {
|
||||
if (errno != ERANGE)
|
||||
break;
|
||||
} else if (len < hint) {
|
||||
} else if (len <= hint) {
|
||||
strbuf_setlen(sb, len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -412,7 +412,7 @@ test_expect_success SYMLINKS 're-init to move gitdir symlink' '
|
||||
# Tests for the hidden file attribute on windows
|
||||
is_hidden () {
|
||||
# Use the output of `attrib`, ignore the absolute path
|
||||
case "$(attrib "$1")" in *H*?:*) return 0;; esac
|
||||
case "$("$SYSTEMROOT"/system32/attrib "$1")" in *H*?:*) return 0;; esac
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
102
t/t2031-checkout-long-paths.sh
Executable file
102
t/t2031-checkout-long-paths.sh
Executable file
@@ -0,0 +1,102 @@
|
||||
#!/bin/sh
|
||||
|
||||
test_description='checkout long paths on Windows
|
||||
|
||||
Ensures that Git for Windows can deal with long paths (>260) enabled via core.longpaths'
|
||||
|
||||
. ./test-lib.sh
|
||||
|
||||
if test_have_prereq !MINGW
|
||||
then
|
||||
skip_all='skipping MINGW specific long paths test'
|
||||
test_done
|
||||
fi
|
||||
|
||||
test_expect_success setup '
|
||||
p=longpathxx && # -> 10
|
||||
p=$p$p$p$p$p && # -> 50
|
||||
p=$p$p$p$p$p && # -> 250
|
||||
|
||||
path=${p}/longtestfile && # -> 263 (MAX_PATH = 260)
|
||||
|
||||
blob=$(echo foobar | git hash-object -w --stdin) &&
|
||||
|
||||
printf "100644 %s 0\t%s\n" "$blob" "$path" |
|
||||
git update-index --add --index-info &&
|
||||
git commit -m initial -q
|
||||
'
|
||||
|
||||
test_expect_success 'checkout of long paths without core.longpaths fails' '
|
||||
git config core.longpaths false &&
|
||||
test_must_fail git checkout -f 2>error &&
|
||||
grep -q "Filename too long" error &&
|
||||
test ! -d longpa*
|
||||
'
|
||||
|
||||
test_expect_success 'checkout of long paths with core.longpaths works' '
|
||||
git config core.longpaths true &&
|
||||
git checkout -f &&
|
||||
test_path_is_file longpa*/longtestfile
|
||||
'
|
||||
|
||||
test_expect_success 'update of long paths' '
|
||||
echo frotz >>$(ls longpa*/longtestfile) &&
|
||||
echo $path > expect &&
|
||||
git ls-files -m > actual &&
|
||||
test_cmp expect actual &&
|
||||
git add $path &&
|
||||
git commit -m second &&
|
||||
git grep "frotz" HEAD -- $path
|
||||
'
|
||||
|
||||
test_expect_success cleanup '
|
||||
# bash cannot delete the trash dir if it contains a long path
|
||||
# lets help cleaning up (unless in debug mode)
|
||||
if test -z "$debug"
|
||||
then
|
||||
rm -rf longpa~1
|
||||
fi
|
||||
'
|
||||
|
||||
# check that the template used in the test won't be too long:
|
||||
abspath="$(pwd)"/testdir
|
||||
test ${#abspath} -gt 230 ||
|
||||
test_set_prereq SHORTABSPATH
|
||||
|
||||
test_expect_success SHORTABSPATH 'clean up path close to MAX_PATH' '
|
||||
p=/123456789abcdef/123456789abcdef/123456789abcdef/123456789abc/ef &&
|
||||
p=y$p$p$p$p &&
|
||||
subdir="x$(echo "$p" | tail -c $((253 - ${#abspath})) - )" &&
|
||||
# Now, $abspath/$subdir has exactly 254 characters, and is inside CWD
|
||||
p2="$abspath/$subdir" &&
|
||||
test 254 = ${#p2} &&
|
||||
|
||||
# Be careful to overcome path limitations of the MSys tools and split
|
||||
# the $subdir into two parts. ($subdir2 has to contain 16 chars and a
|
||||
# slash somewhere following; that is why we asked for abspath <= 230 and
|
||||
# why we placed a slash near the end of the $subdir template.)
|
||||
subdir2=${subdir#????????????????*/} &&
|
||||
subdir1=testdir/${subdir%/$subdir2} &&
|
||||
mkdir -p "$subdir1" &&
|
||||
i=0 &&
|
||||
# The most important case is when absolute path is 258 characters long,
|
||||
# and that will be when i == 4.
|
||||
while test $i -le 7
|
||||
do
|
||||
mkdir -p $subdir2 &&
|
||||
touch $subdir2/one-file &&
|
||||
mv ${subdir2%%/*} "$subdir1/" &&
|
||||
subdir2=z${subdir2} &&
|
||||
i=$(($i+1)) ||
|
||||
exit 1
|
||||
done &&
|
||||
|
||||
# now check that git is able to clear the tree:
|
||||
(cd testdir &&
|
||||
git init &&
|
||||
git config core.longpaths yes &&
|
||||
git clean -fdx) &&
|
||||
test ! -d "$subdir1"
|
||||
'
|
||||
|
||||
test_done
|
||||
51
t/t2040-checkout-symlink-attr.sh
Executable file
51
t/t2040-checkout-symlink-attr.sh
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/bin/sh
|
||||
|
||||
test_description='checkout symlinks with `symlink` attribute on Windows
|
||||
|
||||
Ensures that Git for Windows creates symlinks of the right type,
|
||||
as specified by the `symlink` attribute in `.gitattributes`.'
|
||||
|
||||
# Tell MSYS to create native symlinks. Without this flag test-lib's
|
||||
# prerequisite detection for SYMLINKS doesn't detect the right thing.
|
||||
MSYS=winsymlinks:nativestrict && export MSYS
|
||||
|
||||
. ./test-lib.sh
|
||||
|
||||
if ! test_have_prereq MINGW,SYMLINKS
|
||||
then
|
||||
skip_all='skipping $0: MinGW-only test, which requires symlink support.'
|
||||
test_done
|
||||
fi
|
||||
|
||||
# Adds a symlink to the index without clobbering the work tree.
|
||||
cache_symlink () {
|
||||
sha=$(printf '%s' "$1" | git hash-object --stdin -w) &&
|
||||
git update-index --add --cacheinfo 120000,$sha,"$2"
|
||||
}
|
||||
|
||||
# MSYS2 is very forgiving, it will resolve symlinks even if the
|
||||
# symlink type isn't correct. To make this test meaningful, try
|
||||
# them with a native, non-MSYS executable.
|
||||
cat_native () {
|
||||
filename=$(cygpath -w "$1") &&
|
||||
cmd.exe /c "type \"$filename\""
|
||||
}
|
||||
|
||||
test_expect_success 'checkout symlinks with attr' '
|
||||
cache_symlink file1 file-link &&
|
||||
cache_symlink dir dir-link &&
|
||||
|
||||
printf "file-link symlink=file\ndir-link symlink=dir\n" >.gitattributes &&
|
||||
git add .gitattributes &&
|
||||
|
||||
git checkout . &&
|
||||
|
||||
mkdir dir &&
|
||||
echo "contents1" >file1 &&
|
||||
echo "contents2" >dir/file2 &&
|
||||
|
||||
test "$(cat_native file-link)" = "contents1" &&
|
||||
test "$(cat_native dir-link/file2)" = "contents2"
|
||||
'
|
||||
|
||||
test_done
|
||||
@@ -95,7 +95,7 @@ test_expect_success 'clone -c remote.<remote>.fetch=<refspec> --origin=<name>' '
|
||||
# Tests for the hidden file attribute on windows
|
||||
is_hidden () {
|
||||
# Use the output of `attrib`, ignore the absolute path
|
||||
case "$(attrib "$1")" in *H*?:*) return 0;; esac
|
||||
case "$("$SYSTEMROOT"/system32/attrib "$1")" in *H*?:*) return 0;; esac
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
@@ -1345,12 +1345,6 @@ test_expect_success GPG \
|
||||
'test_config user.signingkey BobTheMouse &&
|
||||
test_must_fail git tag -s -m tail tag-gpg-failure'
|
||||
|
||||
# try to produce invalid signature
|
||||
test_expect_success GPG \
|
||||
'git tag -s fails if gpg is misconfigured (bad signature format)' \
|
||||
'test_config gpg.program echo &&
|
||||
test_must_fail git tag -s -m tail tag-gpg-failure'
|
||||
|
||||
# try to sign with bad user.signingkey
|
||||
test_expect_success GPGSM \
|
||||
'git tag -s fails if gpgsm is misconfigured (bad key)' \
|
||||
@@ -1358,13 +1352,6 @@ test_expect_success GPGSM \
|
||||
test_config gpg.format x509 &&
|
||||
test_must_fail git tag -s -m tail tag-gpg-failure'
|
||||
|
||||
# try to produce invalid signature
|
||||
test_expect_success GPGSM \
|
||||
'git tag -s fails if gpgsm is misconfigured (bad signature format)' \
|
||||
'test_config gpg.x509.program echo &&
|
||||
test_config gpg.format x509 &&
|
||||
test_must_fail git tag -s -m tail tag-gpg-failure'
|
||||
|
||||
# try to verify without gpg:
|
||||
|
||||
rm -rf gpghome
|
||||
|
||||
105
t/t7419-submodule-long-path.sh
Executable file
105
t/t7419-submodule-long-path.sh
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Copyright (c) 2013 Doug Kelly
|
||||
#
|
||||
|
||||
test_description='Test submodules with a path near PATH_MAX
|
||||
|
||||
This test verifies that "git submodule" initialization, update and clones work, including with recursive submodules and paths approaching PATH_MAX (260 characters on Windows)
|
||||
'
|
||||
|
||||
TEST_NO_CREATE_REPO=1
|
||||
. ./test-lib.sh
|
||||
|
||||
# cloning a submodule calls is_git_directory("$path/../.git/modules/$path"),
|
||||
# which effectively limits the maximum length to PATH_MAX / 2 minus some
|
||||
# overhead; start with 3 * 36 = 108 chars (test 2 fails if >= 110)
|
||||
longpath36=0123456789abcdefghijklmnopqrstuvwxyz
|
||||
longpath180=$longpath36$longpath36$longpath36$longpath36$longpath36
|
||||
|
||||
# the git database must fit within PATH_MAX, which limits the submodule name
|
||||
# to PATH_MAX - len(pwd) - ~90 (= len("/objects//") + 40-byte sha1 + some
|
||||
# overhead from the test case)
|
||||
pwd=$(pwd)
|
||||
pwdlen=$(echo "$pwd" | wc -c)
|
||||
longpath=$(echo $longpath180 | cut -c 1-$((170-$pwdlen)))
|
||||
|
||||
test_expect_success 'submodule with a long path' '
|
||||
git init --bare remote &&
|
||||
test_create_repo bundle1 &&
|
||||
(
|
||||
cd bundle1 &&
|
||||
test_commit "shoot" &&
|
||||
git rev-parse --verify HEAD >../expect
|
||||
) &&
|
||||
mkdir home &&
|
||||
(
|
||||
cd home &&
|
||||
git clone ../remote test &&
|
||||
cd test &&
|
||||
git submodule add ../bundle1 $longpath &&
|
||||
test_commit "sogood" &&
|
||||
(
|
||||
cd $longpath &&
|
||||
git rev-parse --verify HEAD >actual &&
|
||||
test_cmp ../../../expect actual
|
||||
) &&
|
||||
git push origin master
|
||||
) &&
|
||||
mkdir home2 &&
|
||||
(
|
||||
cd home2 &&
|
||||
git clone ../remote test &&
|
||||
cd test &&
|
||||
git checkout master &&
|
||||
git submodule update --init &&
|
||||
(
|
||||
cd $longpath &&
|
||||
git rev-parse --verify HEAD >actual &&
|
||||
test_cmp ../../../expect actual
|
||||
)
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'recursive submodule with a long path' '
|
||||
git init --bare super &&
|
||||
test_create_repo child &&
|
||||
(
|
||||
cd child &&
|
||||
test_commit "shoot" &&
|
||||
git rev-parse --verify HEAD >../expect
|
||||
) &&
|
||||
test_create_repo parent &&
|
||||
(
|
||||
cd parent &&
|
||||
git submodule add ../child $longpath &&
|
||||
test_commit "aim"
|
||||
) &&
|
||||
mkdir home3 &&
|
||||
(
|
||||
cd home3 &&
|
||||
git clone ../super test &&
|
||||
cd test &&
|
||||
git submodule add ../parent foo &&
|
||||
git submodule update --init --recursive &&
|
||||
test_commit "sogood" &&
|
||||
(
|
||||
cd foo/$longpath &&
|
||||
git rev-parse --verify HEAD >actual &&
|
||||
test_cmp ../../../../expect actual
|
||||
) &&
|
||||
git push origin master
|
||||
) &&
|
||||
mkdir home4 &&
|
||||
(
|
||||
cd home4 &&
|
||||
git clone ../super test --recursive &&
|
||||
(
|
||||
cd test/foo/$longpath &&
|
||||
git rev-parse --verify HEAD >actual &&
|
||||
test_cmp ../../../../expect actual
|
||||
)
|
||||
)
|
||||
'
|
||||
|
||||
test_done
|
||||
@@ -1180,8 +1180,8 @@ test_expect_success $PREREQ 'in-reply-to but no threading' '
|
||||
--to=nobody@example.com \
|
||||
--in-reply-to="<in-reply-id@example.com>" \
|
||||
--no-thread \
|
||||
$patches |
|
||||
grep "In-Reply-To: <in-reply-id@example.com>"
|
||||
$patches >out &&
|
||||
grep "In-Reply-To: <in-reply-id@example.com>" out
|
||||
'
|
||||
|
||||
test_expect_success $PREREQ 'no in-reply-to and no threading' '
|
||||
|
||||
@@ -12,6 +12,12 @@ then
|
||||
test_done
|
||||
fi
|
||||
|
||||
if test_have_prereq MINGW
|
||||
then
|
||||
skip_all='skipping remote-svn tests for lack of POSIX'
|
||||
test_done
|
||||
fi
|
||||
|
||||
# Override svnrdump with our simulator
|
||||
PATH="$HOME:$PATH"
|
||||
export PATH PYTHON_PATH GIT_BUILD_DIR
|
||||
|
||||
@@ -43,14 +43,18 @@ test_expect_success 'setup repository and import' '
|
||||
|
||||
test_expect_success 'run log' "
|
||||
git reset --hard origin/a &&
|
||||
git svn log -r2 origin/trunk | grep ^r2 &&
|
||||
git svn log -r4 origin/trunk | grep ^r4 &&
|
||||
git svn log -r3 | grep ^r3
|
||||
git svn log -r2 origin/trunk >out &&
|
||||
grep ^r2 out &&
|
||||
git svn log -r4 origin/trunk >out &&
|
||||
grep ^r4 out &&
|
||||
git svn log -r3 >out &&
|
||||
grep ^r3 out
|
||||
"
|
||||
|
||||
test_expect_success 'run log against a from trunk' "
|
||||
git reset --hard origin/trunk &&
|
||||
git svn log -r3 origin/a | grep ^r3
|
||||
git svn log -r3 origin/a >out &&
|
||||
grep ^r3 out
|
||||
"
|
||||
|
||||
printf 'r1 \nr2 \nr4 \n' > expected-range-r1-r2-r4
|
||||
|
||||
Reference in New Issue
Block a user