mirror of
https://git.proxmox.com/git/libgit2
synced 2025-05-29 15:10:02 +00:00
Merge pull request #492 from carlosmn/networking
Networking improvements
This commit is contained in:
commit
bec92f78bf
@ -28,16 +28,18 @@ GIT_BEGIN_DECL
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new unnamed remote
|
* Create a remote in memory
|
||||||
*
|
*
|
||||||
* Useful when you don't want to store the remote
|
* Create a remote with the default refspecs in memory. You can use
|
||||||
|
* this when you have a URL instead of a remote's name.
|
||||||
*
|
*
|
||||||
* @param out pointer to the new remote object
|
* @param out pointer to the new remote object
|
||||||
* @param repo the associtated repository
|
* @param repo the associtated repository
|
||||||
* @param url the remote repository's URL
|
* @param url the remote repository's URL
|
||||||
|
* @param name the remote's name
|
||||||
* @return GIT_SUCCESS or an error code
|
* @return GIT_SUCCESS or an error code
|
||||||
*/
|
*/
|
||||||
int git_remote_new(git_remote **out, git_repository *repo, const char *url);
|
int git_remote_new(git_remote **out, git_repository *repo, const char *url, const char *name);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the information for a particular remote
|
* Get the information for a particular remote
|
||||||
@ -106,21 +108,14 @@ GIT_EXTERN(int) git_remote_connect(struct git_remote *remote, int direction);
|
|||||||
*/
|
*/
|
||||||
GIT_EXTERN(int) git_remote_ls(git_remote *remote, git_headarray *refs);
|
GIT_EXTERN(int) git_remote_ls(git_remote *remote, git_headarray *refs);
|
||||||
|
|
||||||
/**
|
|
||||||
* Negotiate what data needs to be exchanged to synchroize the remtoe
|
|
||||||
* and local references
|
|
||||||
*
|
|
||||||
* @param remote the remote you want to negotiate with
|
|
||||||
*/
|
|
||||||
GIT_EXTERN(int) git_remote_negotiate(git_remote *remote);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download the packfile
|
* Download the packfile
|
||||||
*
|
*
|
||||||
* The packfile is downloaded with a temporary filename, as it's final
|
* Negotiate what objects should be downloaded and download the
|
||||||
* name is not known yet. If there was no packfile needed (all the
|
* packfile with those objects. The packfile is downloaded with a
|
||||||
* objects were available locally), filename will be NULL and the
|
* temporary filename, as it's final name is not known yet. If there
|
||||||
* function will return success.
|
* was no packfile needed (all the objects were available locally),
|
||||||
|
* filename will be NULL and the function will return success.
|
||||||
*
|
*
|
||||||
* @param remote the remote to download from
|
* @param remote the remote to download from
|
||||||
* @param filename where to store the temproray filename
|
* @param filename where to store the temproray filename
|
||||||
@ -128,6 +123,26 @@ GIT_EXTERN(int) git_remote_negotiate(git_remote *remote);
|
|||||||
*/
|
*/
|
||||||
GIT_EXTERN(int) git_remote_download(char **filename, git_remote *remote);
|
GIT_EXTERN(int) git_remote_download(char **filename, git_remote *remote);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the remote is connected
|
||||||
|
*
|
||||||
|
* Check whether the remote's underlying transport is connected to the
|
||||||
|
* remote host.
|
||||||
|
*
|
||||||
|
* @return 1 if it's connected, 0 otherwise.
|
||||||
|
*/
|
||||||
|
GIT_EXTERN(int) git_remote_connected(git_remote *remote);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from the remote
|
||||||
|
*
|
||||||
|
* Close the connection to the remote and free the underlying
|
||||||
|
* transport.
|
||||||
|
*
|
||||||
|
* @param remote the remote to disconnect from
|
||||||
|
*/
|
||||||
|
GIT_EXTERN(void) git_remote_disconnect(git_remote *remote);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Free the memory associated with a remote
|
* Free the memory associated with a remote
|
||||||
*
|
*
|
||||||
|
50
src/protocol.c
Normal file
50
src/protocol.c
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2009-2011 the libgit2 contributors
|
||||||
|
*
|
||||||
|
* This file is part of libgit2, distributed under the GNU GPL v2 with
|
||||||
|
* a Linking Exception. For full terms see the included COPYING file.
|
||||||
|
*/
|
||||||
|
#include "common.h"
|
||||||
|
#include "protocol.h"
|
||||||
|
#include "pkt.h"
|
||||||
|
#include "buffer.h"
|
||||||
|
|
||||||
|
int git_protocol_store_refs(git_protocol *p, const char *data, size_t len)
|
||||||
|
{
|
||||||
|
git_buf *buf = &p->buf;
|
||||||
|
git_vector *refs = p->refs;
|
||||||
|
int error;
|
||||||
|
const char *line_end, *ptr;
|
||||||
|
|
||||||
|
if (len == 0) { /* EOF */
|
||||||
|
if (buf->size != 0)
|
||||||
|
return p->error = git__throw(GIT_ERROR, "EOF and unprocessed data");
|
||||||
|
else
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
git_buf_put(buf, data, len);
|
||||||
|
ptr = buf->ptr;
|
||||||
|
while (1) {
|
||||||
|
git_pkt *pkt;
|
||||||
|
|
||||||
|
if (buf->size == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
error = git_pkt_parse_line(&pkt, ptr, &line_end, buf->size);
|
||||||
|
if (error == GIT_ESHORTBUFFER)
|
||||||
|
return 0; /* Ask for more */
|
||||||
|
if (error < GIT_SUCCESS)
|
||||||
|
return p->error = git__rethrow(error, "Failed to parse pkt-line");
|
||||||
|
|
||||||
|
git_buf_consume(buf, line_end);
|
||||||
|
error = git_vector_insert(refs, pkt);
|
||||||
|
if (error < GIT_SUCCESS)
|
||||||
|
return p->error = git__rethrow(error, "Failed to add pkt to list");
|
||||||
|
|
||||||
|
if (pkt->type == GIT_PKT_FLUSH)
|
||||||
|
p->flush = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return error;
|
||||||
|
}
|
23
src/protocol.h
Normal file
23
src/protocol.h
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2009-2011 the libgit2 contributors
|
||||||
|
*
|
||||||
|
* This file is part of libgit2, distributed under the GNU GPL v2 with
|
||||||
|
* a Linking Exception. For full terms see the included COPYING file.
|
||||||
|
*/
|
||||||
|
#ifndef INCLUDE_protocol_h__
|
||||||
|
#define INCLUDE_protocol_h__
|
||||||
|
|
||||||
|
#include "transport.h"
|
||||||
|
#include "buffer.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
git_transport *transport;
|
||||||
|
git_vector *refs;
|
||||||
|
git_buf buf;
|
||||||
|
int error;
|
||||||
|
unsigned int flush :1;
|
||||||
|
} git_protocol;
|
||||||
|
|
||||||
|
int git_protocol_store_refs(git_protocol *p, const char *data, size_t len);
|
||||||
|
|
||||||
|
#endif
|
@ -23,8 +23,13 @@ int git_refspec_parse(git_refspec *refspec, const char *str)
|
|||||||
}
|
}
|
||||||
|
|
||||||
delim = strchr(str, ':');
|
delim = strchr(str, ':');
|
||||||
if (delim == NULL)
|
if (delim == NULL) {
|
||||||
return git__throw(GIT_EOBJCORRUPTED, "Failed to parse refspec. No ':'");
|
refspec->src = git__strdup(str);
|
||||||
|
if (refspec->src == NULL)
|
||||||
|
return GIT_ENOMEM;
|
||||||
|
|
||||||
|
return GIT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
refspec->src = git__strndup(str, delim - str);
|
refspec->src = git__strndup(str, delim - str);
|
||||||
if (refspec->src == NULL)
|
if (refspec->src == NULL)
|
||||||
|
@ -10,9 +10,12 @@
|
|||||||
#include "git2/refspec.h"
|
#include "git2/refspec.h"
|
||||||
|
|
||||||
struct git_refspec {
|
struct git_refspec {
|
||||||
int force;
|
struct git_refspec *next;
|
||||||
char *src;
|
char *src;
|
||||||
char *dst;
|
char *dst;
|
||||||
|
unsigned int force :1,
|
||||||
|
pattern :1,
|
||||||
|
matching :1;
|
||||||
};
|
};
|
||||||
|
|
||||||
int git_refspec_parse(struct git_refspec *refspec, const char *str);
|
int git_refspec_parse(struct git_refspec *refspec, const char *str);
|
||||||
|
46
src/remote.c
46
src/remote.c
@ -56,22 +56,34 @@ static int parse_remote_refspec(git_config *cfg, git_refspec *refspec, const cha
|
|||||||
return refspec_parse(refspec, val);
|
return refspec_parse(refspec, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
int git_remote_new(git_remote **out, git_repository *repo, const char *url)
|
int git_remote_new(git_remote **out, git_repository *repo, const char *url, const char *name)
|
||||||
{
|
{
|
||||||
git_remote *remote;
|
git_remote *remote;
|
||||||
|
|
||||||
|
if (url == NULL)
|
||||||
|
return git__throw(GIT_EINVALIDARGS, "No URL was given");
|
||||||
|
|
||||||
remote = git__malloc(sizeof(git_remote));
|
remote = git__malloc(sizeof(git_remote));
|
||||||
if (remote == NULL)
|
if (remote == NULL)
|
||||||
return GIT_ENOMEM;
|
return GIT_ENOMEM;
|
||||||
|
|
||||||
memset(remote, 0x0, sizeof(git_remote));
|
memset(remote, 0x0, sizeof(git_remote));
|
||||||
remote->repo = repo;
|
remote->repo = repo;
|
||||||
|
|
||||||
remote->url = git__strdup(url);
|
remote->url = git__strdup(url);
|
||||||
if (remote->url == NULL) {
|
if (remote->url == NULL) {
|
||||||
git__free(remote);
|
git__free(remote);
|
||||||
return GIT_ENOMEM;
|
return GIT_ENOMEM;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name != NULL) {
|
||||||
|
remote->name = git__strdup(name);
|
||||||
|
if (remote->name == NULL) {
|
||||||
|
git__free(remote);
|
||||||
|
return GIT_ENOMEM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
*out = remote;
|
*out = remote;
|
||||||
return GIT_SUCCESS;
|
return GIT_SUCCESS;
|
||||||
}
|
}
|
||||||
@ -206,13 +218,13 @@ int git_remote_ls(git_remote *remote, git_headarray *refs)
|
|||||||
return remote->transport->ls(remote->transport, refs);
|
return remote->transport->ls(remote->transport, refs);
|
||||||
}
|
}
|
||||||
|
|
||||||
int git_remote_negotiate(git_remote *remote)
|
|
||||||
{
|
|
||||||
return git_fetch_negotiate(remote);
|
|
||||||
}
|
|
||||||
|
|
||||||
int git_remote_download(char **filename, git_remote *remote)
|
int git_remote_download(char **filename, git_remote *remote)
|
||||||
{
|
{
|
||||||
|
int error;
|
||||||
|
|
||||||
|
if ((error = git_fetch_negotiate(remote)) < 0)
|
||||||
|
return git__rethrow(error, "Error negotiating");
|
||||||
|
|
||||||
return git_fetch_download_pack(filename, remote);
|
return git_fetch_download_pack(filename, remote);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,6 +267,21 @@ int git_remote_update_tips(struct git_remote *remote)
|
|||||||
return GIT_SUCCESS;
|
return GIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int git_remote_connected(git_remote *remote)
|
||||||
|
{
|
||||||
|
return remote->transport == NULL ? 0 : remote->transport->connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
void git_remote_disconnect(git_remote *remote)
|
||||||
|
{
|
||||||
|
if (remote->transport != NULL) {
|
||||||
|
if (remote->transport->connected)
|
||||||
|
remote->transport->close(remote->transport);
|
||||||
|
|
||||||
|
remote->transport->free(remote->transport);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void git_remote_free(git_remote *remote)
|
void git_remote_free(git_remote *remote)
|
||||||
{
|
{
|
||||||
if (remote == NULL)
|
if (remote == NULL)
|
||||||
@ -266,11 +293,6 @@ void git_remote_free(git_remote *remote)
|
|||||||
git__free(remote->push.dst);
|
git__free(remote->push.dst);
|
||||||
git__free(remote->url);
|
git__free(remote->url);
|
||||||
git__free(remote->name);
|
git__free(remote->name);
|
||||||
if (remote->transport != NULL) {
|
git_remote_disconnect(remote);
|
||||||
if (remote->transport->connected)
|
|
||||||
remote->transport->close(remote->transport);
|
|
||||||
|
|
||||||
remote->transport->free(remote->transport);
|
|
||||||
}
|
|
||||||
git__free(remote);
|
git__free(remote);
|
||||||
}
|
}
|
||||||
|
@ -20,9 +20,11 @@
|
|||||||
#include "filebuf.h"
|
#include "filebuf.h"
|
||||||
#include "repository.h"
|
#include "repository.h"
|
||||||
#include "fetch.h"
|
#include "fetch.h"
|
||||||
|
#include "protocol.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
git_transport parent;
|
git_transport parent;
|
||||||
|
git_protocol proto;
|
||||||
GIT_SOCKET socket;
|
GIT_SOCKET socket;
|
||||||
git_vector refs;
|
git_vector refs;
|
||||||
git_remote_head **heads;
|
git_remote_head **heads;
|
||||||
@ -126,11 +128,7 @@ static int do_connect(transport_git *t, const char *url)
|
|||||||
static int store_refs(transport_git *t)
|
static int store_refs(transport_git *t)
|
||||||
{
|
{
|
||||||
gitno_buffer *buf = &t->buf;
|
gitno_buffer *buf = &t->buf;
|
||||||
git_vector *refs = &t->refs;
|
|
||||||
int error = GIT_SUCCESS;
|
int error = GIT_SUCCESS;
|
||||||
const char *line_end, *ptr;
|
|
||||||
git_pkt *pkt;
|
|
||||||
|
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
error = gitno_recv(buf);
|
error = gitno_recv(buf);
|
||||||
@ -139,34 +137,20 @@ static int store_refs(transport_git *t)
|
|||||||
if (error == GIT_SUCCESS) /* Orderly shutdown, so exit */
|
if (error == GIT_SUCCESS) /* Orderly shutdown, so exit */
|
||||||
return GIT_SUCCESS;
|
return GIT_SUCCESS;
|
||||||
|
|
||||||
ptr = buf->data;
|
error = git_protocol_store_refs(&t->proto, buf->data, buf->offset);
|
||||||
while (1) {
|
if (error == GIT_ESHORTBUFFER) {
|
||||||
if (buf->offset == 0)
|
gitno_consume_n(buf, buf->len);
|
||||||
break;
|
continue;
|
||||||
error = git_pkt_parse_line(&pkt, ptr, &line_end, buf->offset);
|
}
|
||||||
/*
|
|
||||||
* If the error is GIT_ESHORTBUFFER, it means the buffer
|
|
||||||
* isn't long enough to satisfy the request. Break out and
|
|
||||||
* wait for more input.
|
|
||||||
* On any other error, fail.
|
|
||||||
*/
|
|
||||||
if (error == GIT_ESHORTBUFFER) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (error < GIT_SUCCESS) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Get rid of the part we've used already */
|
if (error < GIT_SUCCESS)
|
||||||
gitno_consume(buf, line_end);
|
return git__rethrow(error, "Failed to store refs");
|
||||||
|
|
||||||
error = git_vector_insert(refs, pkt);
|
gitno_consume_n(buf, buf->offset);
|
||||||
if (error < GIT_SUCCESS)
|
|
||||||
return error;
|
|
||||||
|
|
||||||
if (pkt->type == GIT_PKT_FLUSH)
|
|
||||||
return GIT_SUCCESS;
|
|
||||||
|
|
||||||
|
if (t->proto.flush) { /* No more refs */
|
||||||
|
t->proto.flush = 0;
|
||||||
|
return GIT_SUCCESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -476,6 +460,7 @@ static void git_free(git_transport *transport)
|
|||||||
|
|
||||||
git_vector_free(refs);
|
git_vector_free(refs);
|
||||||
git__free(t->heads);
|
git__free(t->heads);
|
||||||
|
git_buf_free(&t->proto.buf);
|
||||||
git__free(t->parent.url);
|
git__free(t->parent.url);
|
||||||
git__free(t);
|
git__free(t);
|
||||||
}
|
}
|
||||||
@ -501,6 +486,8 @@ int git_transport_git(git_transport **out)
|
|||||||
t->parent.download_pack = git_download_pack;
|
t->parent.download_pack = git_download_pack;
|
||||||
t->parent.close = git_close;
|
t->parent.close = git_close;
|
||||||
t->parent.free = git_free;
|
t->parent.free = git_free;
|
||||||
|
t->proto.refs = &t->refs;
|
||||||
|
t->proto.transport = (git_transport *) t;
|
||||||
|
|
||||||
*out = (git_transport *) t;
|
*out = (git_transport *) t;
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
#include "fetch.h"
|
#include "fetch.h"
|
||||||
#include "filebuf.h"
|
#include "filebuf.h"
|
||||||
#include "repository.h"
|
#include "repository.h"
|
||||||
|
#include "protocol.h"
|
||||||
|
|
||||||
enum last_cb {
|
enum last_cb {
|
||||||
NONE,
|
NONE,
|
||||||
@ -28,6 +29,7 @@ enum last_cb {
|
|||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
git_transport parent;
|
git_transport parent;
|
||||||
|
git_protocol proto;
|
||||||
git_vector refs;
|
git_vector refs;
|
||||||
git_vector common;
|
git_vector common;
|
||||||
int socket;
|
int socket;
|
||||||
@ -186,47 +188,8 @@ static int on_headers_complete(http_parser *parser)
|
|||||||
static int on_body_store_refs(http_parser *parser, const char *str, size_t len)
|
static int on_body_store_refs(http_parser *parser, const char *str, size_t len)
|
||||||
{
|
{
|
||||||
transport_http *t = (transport_http *) parser->data;
|
transport_http *t = (transport_http *) parser->data;
|
||||||
git_buf *buf = &t->buf;
|
|
||||||
git_vector *refs = &t->refs;
|
|
||||||
int error;
|
|
||||||
const char *line_end, *ptr;
|
|
||||||
static int first_pkt = 1;
|
|
||||||
|
|
||||||
if (len == 0) { /* EOF */
|
return git_protocol_store_refs(&t->proto, str, len);
|
||||||
if (buf->size != 0)
|
|
||||||
return t->error = git__throw(GIT_ERROR, "EOF and unprocessed data");
|
|
||||||
else
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
git_buf_put(buf, str, len);
|
|
||||||
ptr = buf->ptr;
|
|
||||||
while (1) {
|
|
||||||
git_pkt *pkt;
|
|
||||||
|
|
||||||
if (buf->size == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
error = git_pkt_parse_line(&pkt, ptr, &line_end, buf->size);
|
|
||||||
if (error == GIT_ESHORTBUFFER)
|
|
||||||
return 0; /* Ask for more */
|
|
||||||
if (error < GIT_SUCCESS)
|
|
||||||
return t->error = git__rethrow(error, "Failed to parse pkt-line");
|
|
||||||
|
|
||||||
git_buf_consume(buf, line_end);
|
|
||||||
|
|
||||||
if (first_pkt) {
|
|
||||||
first_pkt = 0;
|
|
||||||
if (pkt->type != GIT_PKT_COMMENT)
|
|
||||||
return t->error = git__throw(GIT_EOBJCORRUPTED, "Not a valid smart HTTP response");
|
|
||||||
}
|
|
||||||
|
|
||||||
error = git_vector_insert(refs, pkt);
|
|
||||||
if (error < GIT_SUCCESS)
|
|
||||||
return t->error = git__rethrow(error, "Failed to add pkt to list");
|
|
||||||
}
|
|
||||||
|
|
||||||
return error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int on_message_complete(http_parser *parser)
|
static int on_message_complete(http_parser *parser)
|
||||||
@ -243,6 +206,7 @@ static int store_refs(transport_http *t)
|
|||||||
http_parser_settings settings;
|
http_parser_settings settings;
|
||||||
char buffer[1024];
|
char buffer[1024];
|
||||||
gitno_buffer buf;
|
gitno_buffer buf;
|
||||||
|
git_pkt *pkt;
|
||||||
|
|
||||||
http_parser_init(&t->parser, HTTP_RESPONSE);
|
http_parser_init(&t->parser, HTTP_RESPONSE);
|
||||||
t->parser.data = t;
|
t->parser.data = t;
|
||||||
@ -273,6 +237,12 @@ static int store_refs(transport_http *t)
|
|||||||
return GIT_SUCCESS;
|
return GIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pkt = git_vector_get(&t->refs, 0);
|
||||||
|
if (pkt == NULL || pkt->type != GIT_PKT_COMMENT)
|
||||||
|
return t->error = git__throw(GIT_EOBJCORRUPTED, "Not a valid smart HTTP response");
|
||||||
|
else
|
||||||
|
git_vector_remove(&t->refs, 0);
|
||||||
|
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -750,6 +720,7 @@ static void http_free(git_transport *transport)
|
|||||||
}
|
}
|
||||||
git_vector_free(common);
|
git_vector_free(common);
|
||||||
git_buf_free(&t->buf);
|
git_buf_free(&t->buf);
|
||||||
|
git_buf_free(&t->proto.buf);
|
||||||
git__free(t->heads);
|
git__free(t->heads);
|
||||||
git__free(t->content_type);
|
git__free(t->content_type);
|
||||||
git__free(t->host);
|
git__free(t->host);
|
||||||
@ -775,6 +746,8 @@ int git_transport_http(git_transport **out)
|
|||||||
t->parent.download_pack = http_download_pack;
|
t->parent.download_pack = http_download_pack;
|
||||||
t->parent.close = http_close;
|
t->parent.close = http_close;
|
||||||
t->parent.free = http_free;
|
t->parent.free = http_free;
|
||||||
|
t->proto.refs = &t->refs;
|
||||||
|
t->proto.transport = (git_transport *) t;
|
||||||
|
|
||||||
#ifdef GIT_WIN32
|
#ifdef GIT_WIN32
|
||||||
/* on win32, the WSA context needs to be initialized
|
/* on win32, the WSA context needs to be initialized
|
||||||
|
Loading…
Reference in New Issue
Block a user