зеркало из https://github.com/microsoft/git.git
Merge branch 'jn/thinner-wrapper'
* jn/thinner-wrapper: Remove pack file handling dependency from wrapper.o pack-objects: mark file-local variable static wrapper: give zlib wrappers their own translation unit strbuf: move strbuf_branchname to sha1_name.c path helpers: move git_mkstemp* to wrapper.c wrapper: move odb_* to environment.c wrapper: move xmmap() to sha1_file.c
This commit is contained in:
Коммит
39f04dbaac
1
Makefile
1
Makefile
|
@ -670,6 +670,7 @@ LIB_OBJS += write_or_die.o
|
|||
LIB_OBJS += ws.o
|
||||
LIB_OBJS += wt-status.o
|
||||
LIB_OBJS += xdiff-interface.o
|
||||
LIB_OBJS += zlib.o
|
||||
|
||||
BUILTIN_OBJS += builtin/add.o
|
||||
BUILTIN_OBJS += builtin/annotate.o
|
||||
|
|
|
@ -1543,7 +1543,7 @@ static void try_to_free_from_threads(size_t size)
|
|||
read_unlock();
|
||||
}
|
||||
|
||||
try_to_free_t old_try_to_free_routine;
|
||||
static try_to_free_t old_try_to_free_routine;
|
||||
|
||||
/*
|
||||
* The main thread waits on the condition that (at least) one of the workers
|
||||
|
|
|
@ -172,6 +172,43 @@ char *get_object_directory(void)
|
|||
return git_object_dir;
|
||||
}
|
||||
|
||||
int odb_mkstemp(char *template, size_t limit, const char *pattern)
|
||||
{
|
||||
int fd;
|
||||
/*
|
||||
* we let the umask do its job, don't try to be more
|
||||
* restrictive except to remove write permission.
|
||||
*/
|
||||
int mode = 0444;
|
||||
snprintf(template, limit, "%s/%s",
|
||||
get_object_directory(), pattern);
|
||||
fd = git_mkstemp_mode(template, mode);
|
||||
if (0 <= fd)
|
||||
return fd;
|
||||
|
||||
/* slow path */
|
||||
/* some mkstemp implementations erase template on failure */
|
||||
snprintf(template, limit, "%s/%s",
|
||||
get_object_directory(), pattern);
|
||||
safe_create_leading_directories(template);
|
||||
return xmkstemp_mode(template, mode);
|
||||
}
|
||||
|
||||
int odb_pack_keep(char *name, size_t namesz, unsigned char *sha1)
|
||||
{
|
||||
int fd;
|
||||
|
||||
snprintf(name, namesz, "%s/pack/pack-%s.keep",
|
||||
get_object_directory(), sha1_to_hex(sha1));
|
||||
fd = open(name, O_RDWR|O_CREAT|O_EXCL, 0600);
|
||||
if (0 <= fd)
|
||||
return fd;
|
||||
|
||||
/* slow path */
|
||||
safe_create_leading_directories(name);
|
||||
return open(name, O_RDWR|O_CREAT|O_EXCL, 0600);
|
||||
}
|
||||
|
||||
char *get_index_file(void)
|
||||
{
|
||||
if (!git_index_file)
|
||||
|
|
|
@ -421,6 +421,7 @@ extern ssize_t xwrite(int fd, const void *buf, size_t len);
|
|||
extern int xdup(int fd);
|
||||
extern FILE *xfdopen(int fd, const char *mode);
|
||||
extern int xmkstemp(char *template);
|
||||
extern int xmkstemp_mode(char *template, int mode);
|
||||
extern int odb_mkstemp(char *template, size_t limit, const char *pattern);
|
||||
extern int odb_pack_keep(char *name, size_t namesz, unsigned char *sha1);
|
||||
|
||||
|
|
113
path.c
113
path.c
|
@ -161,119 +161,6 @@ char *git_path_submodule(const char *path, const char *fmt, ...)
|
|||
return cleanup_path(pathname);
|
||||
}
|
||||
|
||||
/* git_mkstemp() - create tmp file honoring TMPDIR variable */
|
||||
int git_mkstemp(char *path, size_t len, const char *template)
|
||||
{
|
||||
const char *tmp;
|
||||
size_t n;
|
||||
|
||||
tmp = getenv("TMPDIR");
|
||||
if (!tmp)
|
||||
tmp = "/tmp";
|
||||
n = snprintf(path, len, "%s/%s", tmp, template);
|
||||
if (len <= n) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
return mkstemp(path);
|
||||
}
|
||||
|
||||
/* git_mkstemps() - create tmp file with suffix honoring TMPDIR variable. */
|
||||
int git_mkstemps(char *path, size_t len, const char *template, int suffix_len)
|
||||
{
|
||||
const char *tmp;
|
||||
size_t n;
|
||||
|
||||
tmp = getenv("TMPDIR");
|
||||
if (!tmp)
|
||||
tmp = "/tmp";
|
||||
n = snprintf(path, len, "%s/%s", tmp, template);
|
||||
if (len <= n) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
return mkstemps(path, suffix_len);
|
||||
}
|
||||
|
||||
/* Adapted from libiberty's mkstemp.c. */
|
||||
|
||||
#undef TMP_MAX
|
||||
#define TMP_MAX 16384
|
||||
|
||||
int git_mkstemps_mode(char *pattern, int suffix_len, int mode)
|
||||
{
|
||||
static const char letters[] =
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"0123456789";
|
||||
static const int num_letters = 62;
|
||||
uint64_t value;
|
||||
struct timeval tv;
|
||||
char *template;
|
||||
size_t len;
|
||||
int fd, count;
|
||||
|
||||
len = strlen(pattern);
|
||||
|
||||
if (len < 6 + suffix_len) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (strncmp(&pattern[len - 6 - suffix_len], "XXXXXX", 6)) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Replace pattern's XXXXXX characters with randomness.
|
||||
* Try TMP_MAX different filenames.
|
||||
*/
|
||||
gettimeofday(&tv, NULL);
|
||||
value = ((size_t)(tv.tv_usec << 16)) ^ tv.tv_sec ^ getpid();
|
||||
template = &pattern[len - 6 - suffix_len];
|
||||
for (count = 0; count < TMP_MAX; ++count) {
|
||||
uint64_t v = value;
|
||||
/* Fill in the random bits. */
|
||||
template[0] = letters[v % num_letters]; v /= num_letters;
|
||||
template[1] = letters[v % num_letters]; v /= num_letters;
|
||||
template[2] = letters[v % num_letters]; v /= num_letters;
|
||||
template[3] = letters[v % num_letters]; v /= num_letters;
|
||||
template[4] = letters[v % num_letters]; v /= num_letters;
|
||||
template[5] = letters[v % num_letters]; v /= num_letters;
|
||||
|
||||
fd = open(pattern, O_CREAT | O_EXCL | O_RDWR, mode);
|
||||
if (fd > 0)
|
||||
return fd;
|
||||
/*
|
||||
* Fatal error (EPERM, ENOSPC etc).
|
||||
* It doesn't make sense to loop.
|
||||
*/
|
||||
if (errno != EEXIST)
|
||||
break;
|
||||
/*
|
||||
* This is a random value. It is only necessary that
|
||||
* the next TMP_MAX values generated by adding 7777 to
|
||||
* VALUE are different with (module 2^32).
|
||||
*/
|
||||
value += 7777;
|
||||
}
|
||||
/* We return the null string if we can't find a unique file name. */
|
||||
pattern[0] = '\0';
|
||||
return -1;
|
||||
}
|
||||
|
||||
int git_mkstemp_mode(char *pattern, int mode)
|
||||
{
|
||||
/* mkstemp is just mkstemps with no suffix */
|
||||
return git_mkstemps_mode(pattern, 0, mode);
|
||||
}
|
||||
|
||||
int gitmkstemps(char *pattern, int suffix_len)
|
||||
{
|
||||
return git_mkstemps_mode(pattern, suffix_len, 0600);
|
||||
}
|
||||
|
||||
int validate_headref(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
|
|
26
sha1_file.c
26
sha1_file.c
|
@ -578,6 +578,21 @@ void release_pack_memory(size_t need, int fd)
|
|||
; /* nothing */
|
||||
}
|
||||
|
||||
void *xmmap(void *start, size_t length,
|
||||
int prot, int flags, int fd, off_t offset)
|
||||
{
|
||||
void *ret = mmap(start, length, prot, flags, fd, offset);
|
||||
if (ret == MAP_FAILED) {
|
||||
if (!length)
|
||||
return NULL;
|
||||
release_pack_memory(length, fd);
|
||||
ret = mmap(start, length, prot, flags, fd, offset);
|
||||
if (ret == MAP_FAILED)
|
||||
die_errno("Out of memory? mmap failed");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void close_pack_windows(struct packed_git *p)
|
||||
{
|
||||
while (p->windows) {
|
||||
|
@ -803,11 +818,22 @@ static struct packed_git *alloc_packed_git(int extra)
|
|||
return p;
|
||||
}
|
||||
|
||||
static void try_to_free_pack_memory(size_t size)
|
||||
{
|
||||
release_pack_memory(size, -1);
|
||||
}
|
||||
|
||||
struct packed_git *add_packed_git(const char *path, int path_len, int local)
|
||||
{
|
||||
static int have_set_try_to_free_routine;
|
||||
struct stat st;
|
||||
struct packed_git *p = alloc_packed_git(path_len + 2);
|
||||
|
||||
if (!have_set_try_to_free_routine) {
|
||||
have_set_try_to_free_routine = 1;
|
||||
set_try_to_free_routine(try_to_free_pack_memory);
|
||||
}
|
||||
|
||||
/*
|
||||
* Make sure a corresponding .pack file exists and that
|
||||
* the index looks sane.
|
||||
|
|
18
sha1_name.c
18
sha1_name.c
|
@ -936,6 +936,24 @@ int interpret_branch_name(const char *name, struct strbuf *buf)
|
|||
return len;
|
||||
}
|
||||
|
||||
int strbuf_branchname(struct strbuf *sb, const char *name)
|
||||
{
|
||||
int len = strlen(name);
|
||||
if (interpret_branch_name(name, sb) == len)
|
||||
return 0;
|
||||
strbuf_add(sb, name, len);
|
||||
return len;
|
||||
}
|
||||
|
||||
int strbuf_check_branch_ref(struct strbuf *sb, const char *name)
|
||||
{
|
||||
strbuf_branchname(sb, name);
|
||||
if (name[0] == '-')
|
||||
return CHECK_REF_FORMAT_ERROR;
|
||||
strbuf_splice(sb, 0, 0, "refs/heads/", 11);
|
||||
return check_ref_format(sb->buf);
|
||||
}
|
||||
|
||||
/*
|
||||
* This is like "get_sha1_basic()", except it allows "sha1 expressions",
|
||||
* notably "xyz^" for "parent of xyz"
|
||||
|
|
18
strbuf.c
18
strbuf.c
|
@ -386,21 +386,3 @@ int strbuf_read_file(struct strbuf *sb, const char *path, size_t hint)
|
|||
|
||||
return len;
|
||||
}
|
||||
|
||||
int strbuf_branchname(struct strbuf *sb, const char *name)
|
||||
{
|
||||
int len = strlen(name);
|
||||
if (interpret_branch_name(name, sb) == len)
|
||||
return 0;
|
||||
strbuf_add(sb, name, len);
|
||||
return len;
|
||||
}
|
||||
|
||||
int strbuf_check_branch_ref(struct strbuf *sb, const char *name)
|
||||
{
|
||||
strbuf_branchname(sb, name);
|
||||
if (name[0] == '-')
|
||||
return CHECK_REF_FORMAT_ERROR;
|
||||
strbuf_splice(sb, 0, 0, "refs/heads/", 11);
|
||||
return check_ref_format(sb->buf);
|
||||
}
|
||||
|
|
230
wrapper.c
230
wrapper.c
|
@ -3,12 +3,11 @@
|
|||
*/
|
||||
#include "cache.h"
|
||||
|
||||
static void try_to_free_builtin(size_t size)
|
||||
static void do_nothing(size_t size)
|
||||
{
|
||||
release_pack_memory(size, -1);
|
||||
}
|
||||
|
||||
static void (*try_to_free_routine)(size_t size) = try_to_free_builtin;
|
||||
static void (*try_to_free_routine)(size_t size) = do_nothing;
|
||||
|
||||
try_to_free_t set_try_to_free_routine(try_to_free_t routine)
|
||||
{
|
||||
|
@ -108,21 +107,6 @@ void *xcalloc(size_t nmemb, size_t size)
|
|||
return ret;
|
||||
}
|
||||
|
||||
void *xmmap(void *start, size_t length,
|
||||
int prot, int flags, int fd, off_t offset)
|
||||
{
|
||||
void *ret = mmap(start, length, prot, flags, fd, offset);
|
||||
if (ret == MAP_FAILED) {
|
||||
if (!length)
|
||||
return NULL;
|
||||
release_pack_memory(length, fd);
|
||||
ret = mmap(start, length, prot, flags, fd, offset);
|
||||
if (ret == MAP_FAILED)
|
||||
die_errno("Out of memory? mmap failed");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* xread() is the same a read(), but it automatically restarts read()
|
||||
* operations with a recoverable error (EAGAIN and EINTR). xread()
|
||||
|
@ -219,6 +203,119 @@ int xmkstemp(char *template)
|
|||
return fd;
|
||||
}
|
||||
|
||||
/* git_mkstemp() - create tmp file honoring TMPDIR variable */
|
||||
int git_mkstemp(char *path, size_t len, const char *template)
|
||||
{
|
||||
const char *tmp;
|
||||
size_t n;
|
||||
|
||||
tmp = getenv("TMPDIR");
|
||||
if (!tmp)
|
||||
tmp = "/tmp";
|
||||
n = snprintf(path, len, "%s/%s", tmp, template);
|
||||
if (len <= n) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
return mkstemp(path);
|
||||
}
|
||||
|
||||
/* git_mkstemps() - create tmp file with suffix honoring TMPDIR variable. */
|
||||
int git_mkstemps(char *path, size_t len, const char *template, int suffix_len)
|
||||
{
|
||||
const char *tmp;
|
||||
size_t n;
|
||||
|
||||
tmp = getenv("TMPDIR");
|
||||
if (!tmp)
|
||||
tmp = "/tmp";
|
||||
n = snprintf(path, len, "%s/%s", tmp, template);
|
||||
if (len <= n) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
return mkstemps(path, suffix_len);
|
||||
}
|
||||
|
||||
/* Adapted from libiberty's mkstemp.c. */
|
||||
|
||||
#undef TMP_MAX
|
||||
#define TMP_MAX 16384
|
||||
|
||||
int git_mkstemps_mode(char *pattern, int suffix_len, int mode)
|
||||
{
|
||||
static const char letters[] =
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"0123456789";
|
||||
static const int num_letters = 62;
|
||||
uint64_t value;
|
||||
struct timeval tv;
|
||||
char *template;
|
||||
size_t len;
|
||||
int fd, count;
|
||||
|
||||
len = strlen(pattern);
|
||||
|
||||
if (len < 6 + suffix_len) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (strncmp(&pattern[len - 6 - suffix_len], "XXXXXX", 6)) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Replace pattern's XXXXXX characters with randomness.
|
||||
* Try TMP_MAX different filenames.
|
||||
*/
|
||||
gettimeofday(&tv, NULL);
|
||||
value = ((size_t)(tv.tv_usec << 16)) ^ tv.tv_sec ^ getpid();
|
||||
template = &pattern[len - 6 - suffix_len];
|
||||
for (count = 0; count < TMP_MAX; ++count) {
|
||||
uint64_t v = value;
|
||||
/* Fill in the random bits. */
|
||||
template[0] = letters[v % num_letters]; v /= num_letters;
|
||||
template[1] = letters[v % num_letters]; v /= num_letters;
|
||||
template[2] = letters[v % num_letters]; v /= num_letters;
|
||||
template[3] = letters[v % num_letters]; v /= num_letters;
|
||||
template[4] = letters[v % num_letters]; v /= num_letters;
|
||||
template[5] = letters[v % num_letters]; v /= num_letters;
|
||||
|
||||
fd = open(pattern, O_CREAT | O_EXCL | O_RDWR, mode);
|
||||
if (fd > 0)
|
||||
return fd;
|
||||
/*
|
||||
* Fatal error (EPERM, ENOSPC etc).
|
||||
* It doesn't make sense to loop.
|
||||
*/
|
||||
if (errno != EEXIST)
|
||||
break;
|
||||
/*
|
||||
* This is a random value. It is only necessary that
|
||||
* the next TMP_MAX values generated by adding 7777 to
|
||||
* VALUE are different with (module 2^32).
|
||||
*/
|
||||
value += 7777;
|
||||
}
|
||||
/* We return the null string if we can't find a unique file name. */
|
||||
pattern[0] = '\0';
|
||||
return -1;
|
||||
}
|
||||
|
||||
int git_mkstemp_mode(char *pattern, int mode)
|
||||
{
|
||||
/* mkstemp is just mkstemps with no suffix */
|
||||
return git_mkstemps_mode(pattern, 0, mode);
|
||||
}
|
||||
|
||||
int gitmkstemps(char *pattern, int suffix_len)
|
||||
{
|
||||
return git_mkstemps_mode(pattern, suffix_len, 0600);
|
||||
}
|
||||
|
||||
int xmkstemp_mode(char *template, int mode)
|
||||
{
|
||||
int fd;
|
||||
|
@ -229,103 +326,6 @@ int xmkstemp_mode(char *template, int mode)
|
|||
return fd;
|
||||
}
|
||||
|
||||
/*
|
||||
* zlib wrappers to make sure we don't silently miss errors
|
||||
* at init time.
|
||||
*/
|
||||
void git_inflate_init(z_streamp strm)
|
||||
{
|
||||
const char *err;
|
||||
|
||||
switch (inflateInit(strm)) {
|
||||
case Z_OK:
|
||||
return;
|
||||
|
||||
case Z_MEM_ERROR:
|
||||
err = "out of memory";
|
||||
break;
|
||||
case Z_VERSION_ERROR:
|
||||
err = "wrong version";
|
||||
break;
|
||||
default:
|
||||
err = "error";
|
||||
}
|
||||
die("inflateInit: %s (%s)", err, strm->msg ? strm->msg : "no message");
|
||||
}
|
||||
|
||||
void git_inflate_end(z_streamp strm)
|
||||
{
|
||||
if (inflateEnd(strm) != Z_OK)
|
||||
error("inflateEnd: %s", strm->msg ? strm->msg : "failed");
|
||||
}
|
||||
|
||||
int git_inflate(z_streamp strm, int flush)
|
||||
{
|
||||
int ret = inflate(strm, flush);
|
||||
const char *err;
|
||||
|
||||
switch (ret) {
|
||||
/* Out of memory is fatal. */
|
||||
case Z_MEM_ERROR:
|
||||
die("inflate: out of memory");
|
||||
|
||||
/* Data corruption errors: we may want to recover from them (fsck) */
|
||||
case Z_NEED_DICT:
|
||||
err = "needs dictionary"; break;
|
||||
case Z_DATA_ERROR:
|
||||
err = "data stream error"; break;
|
||||
case Z_STREAM_ERROR:
|
||||
err = "stream consistency error"; break;
|
||||
default:
|
||||
err = "unknown error"; break;
|
||||
|
||||
/* Z_BUF_ERROR: normal, needs more space in the output buffer */
|
||||
case Z_BUF_ERROR:
|
||||
case Z_OK:
|
||||
case Z_STREAM_END:
|
||||
return ret;
|
||||
}
|
||||
error("inflate: %s (%s)", err, strm->msg ? strm->msg : "no message");
|
||||
return ret;
|
||||
}
|
||||
|
||||
int odb_mkstemp(char *template, size_t limit, const char *pattern)
|
||||
{
|
||||
int fd;
|
||||
/*
|
||||
* we let the umask do its job, don't try to be more
|
||||
* restrictive except to remove write permission.
|
||||
*/
|
||||
int mode = 0444;
|
||||
snprintf(template, limit, "%s/%s",
|
||||
get_object_directory(), pattern);
|
||||
fd = git_mkstemp_mode(template, mode);
|
||||
if (0 <= fd)
|
||||
return fd;
|
||||
|
||||
/* slow path */
|
||||
/* some mkstemp implementations erase template on failure */
|
||||
snprintf(template, limit, "%s/%s",
|
||||
get_object_directory(), pattern);
|
||||
safe_create_leading_directories(template);
|
||||
return xmkstemp_mode(template, mode);
|
||||
}
|
||||
|
||||
int odb_pack_keep(char *name, size_t namesz, unsigned char *sha1)
|
||||
{
|
||||
int fd;
|
||||
|
||||
snprintf(name, namesz, "%s/pack/pack-%s.keep",
|
||||
get_object_directory(), sha1_to_hex(sha1));
|
||||
fd = open(name, O_RDWR|O_CREAT|O_EXCL, 0600);
|
||||
if (0 <= fd)
|
||||
return fd;
|
||||
|
||||
/* slow path */
|
||||
safe_create_leading_directories(name);
|
||||
return open(name, O_RDWR|O_CREAT|O_EXCL, 0600);
|
||||
}
|
||||
|
||||
static int warn_if_unremovable(const char *op, const char *file, int rc)
|
||||
{
|
||||
if (rc < 0) {
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* zlib wrappers to make sure we don't silently miss errors
|
||||
* at init time.
|
||||
*/
|
||||
#include "cache.h"
|
||||
|
||||
void git_inflate_init(z_streamp strm)
|
||||
{
|
||||
const char *err;
|
||||
|
||||
switch (inflateInit(strm)) {
|
||||
case Z_OK:
|
||||
return;
|
||||
|
||||
case Z_MEM_ERROR:
|
||||
err = "out of memory";
|
||||
break;
|
||||
case Z_VERSION_ERROR:
|
||||
err = "wrong version";
|
||||
break;
|
||||
default:
|
||||
err = "error";
|
||||
}
|
||||
die("inflateInit: %s (%s)", err, strm->msg ? strm->msg : "no message");
|
||||
}
|
||||
|
||||
void git_inflate_end(z_streamp strm)
|
||||
{
|
||||
if (inflateEnd(strm) != Z_OK)
|
||||
error("inflateEnd: %s", strm->msg ? strm->msg : "failed");
|
||||
}
|
||||
|
||||
int git_inflate(z_streamp strm, int flush)
|
||||
{
|
||||
int ret = inflate(strm, flush);
|
||||
const char *err;
|
||||
|
||||
switch (ret) {
|
||||
/* Out of memory is fatal. */
|
||||
case Z_MEM_ERROR:
|
||||
die("inflate: out of memory");
|
||||
|
||||
/* Data corruption errors: we may want to recover from them (fsck) */
|
||||
case Z_NEED_DICT:
|
||||
err = "needs dictionary"; break;
|
||||
case Z_DATA_ERROR:
|
||||
err = "data stream error"; break;
|
||||
case Z_STREAM_ERROR:
|
||||
err = "stream consistency error"; break;
|
||||
default:
|
||||
err = "unknown error"; break;
|
||||
|
||||
/* Z_BUF_ERROR: normal, needs more space in the output buffer */
|
||||
case Z_BUF_ERROR:
|
||||
case Z_OK:
|
||||
case Z_STREAM_END:
|
||||
return ret;
|
||||
}
|
||||
error("inflate: %s (%s)", err, strm->msg ? strm->msg : "no message");
|
||||
return ret;
|
||||
}
|
Загрузка…
Ссылка в новой задаче