зеркало из https://github.com/microsoft/git.git
Merge branch 'ks/combine-diff'
Teach combine-diff to honour the path-output-order imposed by diffcore-order, and optimize how matching paths are found in the N-way diffs made with parents. * ks/combine-diff: tests: add checking that combine-diff emits only correct paths combine-diff: simplify intersect_paths() further combine-diff: combine_diff_path.len is not needed anymore combine-diff: optimize combine_diff_path sets intersection diff test: add tests for combine-diff with orderfile diffcore-order: export generic ordering interface
This commit is contained in:
Коммит
6376463c37
118
combine-diff.c
118
combine-diff.c
|
@ -15,11 +15,10 @@
|
||||||
static struct combine_diff_path *intersect_paths(struct combine_diff_path *curr, int n, int num_parent)
|
static struct combine_diff_path *intersect_paths(struct combine_diff_path *curr, int n, int num_parent)
|
||||||
{
|
{
|
||||||
struct diff_queue_struct *q = &diff_queued_diff;
|
struct diff_queue_struct *q = &diff_queued_diff;
|
||||||
struct combine_diff_path *p;
|
struct combine_diff_path *p, **tail = &curr;
|
||||||
int i;
|
int i, cmp;
|
||||||
|
|
||||||
if (!n) {
|
if (!n) {
|
||||||
struct combine_diff_path *list = NULL, **tail = &list;
|
|
||||||
for (i = 0; i < q->nr; i++) {
|
for (i = 0; i < q->nr; i++) {
|
||||||
int len;
|
int len;
|
||||||
const char *path;
|
const char *path;
|
||||||
|
@ -31,7 +30,6 @@ static struct combine_diff_path *intersect_paths(struct combine_diff_path *curr,
|
||||||
p->path = (char *) &(p->parent[num_parent]);
|
p->path = (char *) &(p->parent[num_parent]);
|
||||||
memcpy(p->path, path, len);
|
memcpy(p->path, path, len);
|
||||||
p->path[len] = 0;
|
p->path[len] = 0;
|
||||||
p->len = len;
|
|
||||||
p->next = NULL;
|
p->next = NULL;
|
||||||
memset(p->parent, 0,
|
memset(p->parent, 0,
|
||||||
sizeof(p->parent[0]) * num_parent);
|
sizeof(p->parent[0]) * num_parent);
|
||||||
|
@ -44,31 +42,37 @@ static struct combine_diff_path *intersect_paths(struct combine_diff_path *curr,
|
||||||
*tail = p;
|
*tail = p;
|
||||||
tail = &p->next;
|
tail = &p->next;
|
||||||
}
|
}
|
||||||
return list;
|
return curr;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (p = curr; p; p = p->next) {
|
/*
|
||||||
int found = 0;
|
* paths in curr (linked list) and q->queue[] (array) are
|
||||||
if (!p->len)
|
* both sorted in the tree order.
|
||||||
continue;
|
*/
|
||||||
for (i = 0; i < q->nr; i++) {
|
i = 0;
|
||||||
const char *path;
|
while ((p = *tail) != NULL) {
|
||||||
int len;
|
cmp = ((i >= q->nr)
|
||||||
|
? -1 : strcmp(p->path, q->queue[i]->two->path));
|
||||||
|
|
||||||
if (diff_unmodified_pair(q->queue[i]))
|
if (cmp < 0) {
|
||||||
continue;
|
/* p->path not in q->queue[]; drop it */
|
||||||
path = q->queue[i]->two->path;
|
*tail = p->next;
|
||||||
len = strlen(path);
|
free(p);
|
||||||
if (len == p->len && !memcmp(path, p->path, len)) {
|
continue;
|
||||||
found = 1;
|
|
||||||
hashcpy(p->parent[n].sha1, q->queue[i]->one->sha1);
|
|
||||||
p->parent[n].mode = q->queue[i]->one->mode;
|
|
||||||
p->parent[n].status = q->queue[i]->status;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!found)
|
|
||||||
p->len = 0;
|
if (cmp > 0) {
|
||||||
|
/* q->queue[i] not in p->path; skip it */
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hashcpy(p->parent[n].sha1, q->queue[i]->one->sha1);
|
||||||
|
p->parent[n].mode = q->queue[i]->one->mode;
|
||||||
|
p->parent[n].status = q->queue[i]->status;
|
||||||
|
|
||||||
|
tail = &p->next;
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
return curr;
|
return curr;
|
||||||
}
|
}
|
||||||
|
@ -1219,8 +1223,6 @@ void show_combined_diff(struct combine_diff_path *p,
|
||||||
{
|
{
|
||||||
struct diff_options *opt = &rev->diffopt;
|
struct diff_options *opt = &rev->diffopt;
|
||||||
|
|
||||||
if (!p->len)
|
|
||||||
return;
|
|
||||||
if (opt->output_format & (DIFF_FORMAT_RAW |
|
if (opt->output_format & (DIFF_FORMAT_RAW |
|
||||||
DIFF_FORMAT_NAME |
|
DIFF_FORMAT_NAME |
|
||||||
DIFF_FORMAT_NAME_STATUS))
|
DIFF_FORMAT_NAME_STATUS))
|
||||||
|
@ -1284,17 +1286,21 @@ static void handle_combined_callback(struct diff_options *opt,
|
||||||
q.queue = xcalloc(num_paths, sizeof(struct diff_filepair *));
|
q.queue = xcalloc(num_paths, sizeof(struct diff_filepair *));
|
||||||
q.alloc = num_paths;
|
q.alloc = num_paths;
|
||||||
q.nr = num_paths;
|
q.nr = num_paths;
|
||||||
for (i = 0, p = paths; p; p = p->next) {
|
for (i = 0, p = paths; p; p = p->next)
|
||||||
if (!p->len)
|
|
||||||
continue;
|
|
||||||
q.queue[i++] = combined_pair(p, num_parent);
|
q.queue[i++] = combined_pair(p, num_parent);
|
||||||
}
|
|
||||||
opt->format_callback(&q, opt, opt->format_callback_data);
|
opt->format_callback(&q, opt, opt->format_callback_data);
|
||||||
for (i = 0; i < num_paths; i++)
|
for (i = 0; i < num_paths; i++)
|
||||||
free_combined_pair(q.queue[i]);
|
free_combined_pair(q.queue[i]);
|
||||||
free(q.queue);
|
free(q.queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const char *path_path(void *obj)
|
||||||
|
{
|
||||||
|
struct combine_diff_path *path = (struct combine_diff_path *)obj;
|
||||||
|
|
||||||
|
return path->path;
|
||||||
|
}
|
||||||
|
|
||||||
void diff_tree_combined(const unsigned char *sha1,
|
void diff_tree_combined(const unsigned char *sha1,
|
||||||
const struct sha1_array *parents,
|
const struct sha1_array *parents,
|
||||||
int dense,
|
int dense,
|
||||||
|
@ -1310,6 +1316,8 @@ void diff_tree_combined(const unsigned char *sha1,
|
||||||
diffopts.output_format = DIFF_FORMAT_NO_OUTPUT;
|
diffopts.output_format = DIFF_FORMAT_NO_OUTPUT;
|
||||||
DIFF_OPT_SET(&diffopts, RECURSIVE);
|
DIFF_OPT_SET(&diffopts, RECURSIVE);
|
||||||
DIFF_OPT_CLR(&diffopts, ALLOW_EXTERNAL);
|
DIFF_OPT_CLR(&diffopts, ALLOW_EXTERNAL);
|
||||||
|
/* tell diff_tree to emit paths in sorted (=tree) order */
|
||||||
|
diffopts.orderfile = NULL;
|
||||||
|
|
||||||
show_log_first = !!rev->loginfo && !rev->no_commit_id;
|
show_log_first = !!rev->loginfo && !rev->no_commit_id;
|
||||||
needsep = 0;
|
needsep = 0;
|
||||||
|
@ -1335,22 +1343,46 @@ void diff_tree_combined(const unsigned char *sha1,
|
||||||
printf("%s%c", diff_line_prefix(opt),
|
printf("%s%c", diff_line_prefix(opt),
|
||||||
opt->line_termination);
|
opt->line_termination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* if showing diff, show it in requested order */
|
||||||
|
if (diffopts.output_format != DIFF_FORMAT_NO_OUTPUT &&
|
||||||
|
opt->orderfile) {
|
||||||
|
diffcore_order(opt->orderfile);
|
||||||
|
}
|
||||||
|
|
||||||
diff_flush(&diffopts);
|
diff_flush(&diffopts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* find out surviving paths */
|
/* find out number of surviving paths */
|
||||||
for (num_paths = 0, p = paths; p; p = p->next) {
|
for (num_paths = 0, p = paths; p; p = p->next)
|
||||||
if (p->len)
|
num_paths++;
|
||||||
num_paths++;
|
|
||||||
|
/* order paths according to diffcore_order */
|
||||||
|
if (opt->orderfile && num_paths) {
|
||||||
|
struct obj_order *o;
|
||||||
|
|
||||||
|
o = xmalloc(sizeof(*o) * num_paths);
|
||||||
|
for (i = 0, p = paths; p; p = p->next, i++)
|
||||||
|
o[i].obj = p;
|
||||||
|
order_objects(opt->orderfile, path_path, o, num_paths);
|
||||||
|
for (i = 0; i < num_paths - 1; i++) {
|
||||||
|
p = o[i].obj;
|
||||||
|
p->next = o[i+1].obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
p = o[num_paths-1].obj;
|
||||||
|
p->next = NULL;
|
||||||
|
paths = o[0].obj;
|
||||||
|
free(o);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (num_paths) {
|
if (num_paths) {
|
||||||
if (opt->output_format & (DIFF_FORMAT_RAW |
|
if (opt->output_format & (DIFF_FORMAT_RAW |
|
||||||
DIFF_FORMAT_NAME |
|
DIFF_FORMAT_NAME |
|
||||||
DIFF_FORMAT_NAME_STATUS)) {
|
DIFF_FORMAT_NAME_STATUS)) {
|
||||||
for (p = paths; p; p = p->next) {
|
for (p = paths; p; p = p->next)
|
||||||
if (p->len)
|
show_raw_diff(p, num_parent, rev);
|
||||||
show_raw_diff(p, num_parent, rev);
|
|
||||||
}
|
|
||||||
needsep = 1;
|
needsep = 1;
|
||||||
}
|
}
|
||||||
else if (opt->output_format &
|
else if (opt->output_format &
|
||||||
|
@ -1363,11 +1395,9 @@ void diff_tree_combined(const unsigned char *sha1,
|
||||||
if (needsep)
|
if (needsep)
|
||||||
printf("%s%c", diff_line_prefix(opt),
|
printf("%s%c", diff_line_prefix(opt),
|
||||||
opt->line_termination);
|
opt->line_termination);
|
||||||
for (p = paths; p; p = p->next) {
|
for (p = paths; p; p = p->next)
|
||||||
if (p->len)
|
show_patch_diff(p, num_parent, dense,
|
||||||
show_patch_diff(p, num_parent, dense,
|
0, rev);
|
||||||
0, rev);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -125,7 +125,6 @@ int run_diff_files(struct rev_info *revs, unsigned int option)
|
||||||
dpath->path = (char *) &(dpath->parent[5]);
|
dpath->path = (char *) &(dpath->parent[5]);
|
||||||
|
|
||||||
dpath->next = NULL;
|
dpath->next = NULL;
|
||||||
dpath->len = path_len;
|
|
||||||
memcpy(dpath->path, ce->name, path_len);
|
memcpy(dpath->path, ce->name, path_len);
|
||||||
dpath->path[path_len] = '\0';
|
dpath->path[path_len] = '\0';
|
||||||
hashclr(dpath->sha1);
|
hashclr(dpath->sha1);
|
||||||
|
@ -327,7 +326,6 @@ static int show_modified(struct rev_info *revs,
|
||||||
p = xmalloc(combine_diff_path_size(2, pathlen));
|
p = xmalloc(combine_diff_path_size(2, pathlen));
|
||||||
p->path = (char *) &p->parent[2];
|
p->path = (char *) &p->parent[2];
|
||||||
p->next = NULL;
|
p->next = NULL;
|
||||||
p->len = pathlen;
|
|
||||||
memcpy(p->path, new->name, pathlen);
|
memcpy(p->path, new->name, pathlen);
|
||||||
p->path[pathlen] = 0;
|
p->path[pathlen] = 0;
|
||||||
p->mode = mode;
|
p->mode = mode;
|
||||||
|
|
1
diff.h
1
diff.h
|
@ -198,7 +198,6 @@ extern int diff_root_tree_sha1(const unsigned char *new, const char *base,
|
||||||
|
|
||||||
struct combine_diff_path {
|
struct combine_diff_path {
|
||||||
struct combine_diff_path *next;
|
struct combine_diff_path *next;
|
||||||
int len;
|
|
||||||
char *path;
|
char *path;
|
||||||
unsigned int mode;
|
unsigned int mode;
|
||||||
unsigned char sha1[20];
|
unsigned char sha1[20];
|
||||||
|
|
|
@ -57,12 +57,6 @@ static void prepare_order(const char *orderfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct pair_order {
|
|
||||||
struct diff_filepair *pair;
|
|
||||||
int orig_order;
|
|
||||||
int order;
|
|
||||||
};
|
|
||||||
|
|
||||||
static int match_order(const char *path)
|
static int match_order(const char *path)
|
||||||
{
|
{
|
||||||
int i;
|
int i;
|
||||||
|
@ -84,35 +78,54 @@ static int match_order(const char *path)
|
||||||
return order_cnt;
|
return order_cnt;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int compare_pair_order(const void *a_, const void *b_)
|
static int compare_objs_order(const void *a_, const void *b_)
|
||||||
{
|
{
|
||||||
struct pair_order const *a, *b;
|
struct obj_order const *a, *b;
|
||||||
a = (struct pair_order const *)a_;
|
a = (struct obj_order const *)a_;
|
||||||
b = (struct pair_order const *)b_;
|
b = (struct obj_order const *)b_;
|
||||||
if (a->order != b->order)
|
if (a->order != b->order)
|
||||||
return a->order - b->order;
|
return a->order - b->order;
|
||||||
return a->orig_order - b->orig_order;
|
return a->orig_order - b->orig_order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void order_objects(const char *orderfile, obj_path_fn_t obj_path,
|
||||||
|
struct obj_order *objs, int nr)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (!nr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
prepare_order(orderfile);
|
||||||
|
for (i = 0; i < nr; i++) {
|
||||||
|
objs[i].orig_order = i;
|
||||||
|
objs[i].order = match_order(obj_path(objs[i].obj));
|
||||||
|
}
|
||||||
|
qsort(objs, nr, sizeof(*objs), compare_objs_order);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *pair_pathtwo(void *obj)
|
||||||
|
{
|
||||||
|
struct diff_filepair *pair = (struct diff_filepair *)obj;
|
||||||
|
|
||||||
|
return pair->two->path;
|
||||||
|
}
|
||||||
|
|
||||||
void diffcore_order(const char *orderfile)
|
void diffcore_order(const char *orderfile)
|
||||||
{
|
{
|
||||||
struct diff_queue_struct *q = &diff_queued_diff;
|
struct diff_queue_struct *q = &diff_queued_diff;
|
||||||
struct pair_order *o;
|
struct obj_order *o;
|
||||||
int i;
|
int i;
|
||||||
|
|
||||||
if (!q->nr)
|
if (!q->nr)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
o = xmalloc(sizeof(*o) * q->nr);
|
o = xmalloc(sizeof(*o) * q->nr);
|
||||||
prepare_order(orderfile);
|
|
||||||
for (i = 0; i < q->nr; i++) {
|
|
||||||
o[i].pair = q->queue[i];
|
|
||||||
o[i].orig_order = i;
|
|
||||||
o[i].order = match_order(o[i].pair->two->path);
|
|
||||||
}
|
|
||||||
qsort(o, q->nr, sizeof(*o), compare_pair_order);
|
|
||||||
for (i = 0; i < q->nr; i++)
|
for (i = 0; i < q->nr; i++)
|
||||||
q->queue[i] = o[i].pair;
|
o[i].obj = q->queue[i];
|
||||||
|
order_objects(orderfile, pair_pathtwo, o, q->nr);
|
||||||
|
for (i = 0; i < q->nr; i++)
|
||||||
|
q->queue[i] = o[i].obj;
|
||||||
free(o);
|
free(o);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
14
diffcore.h
14
diffcore.h
|
@ -111,6 +111,20 @@ extern void diffcore_merge_broken(void);
|
||||||
extern void diffcore_pickaxe(struct diff_options *);
|
extern void diffcore_pickaxe(struct diff_options *);
|
||||||
extern void diffcore_order(const char *orderfile);
|
extern void diffcore_order(const char *orderfile);
|
||||||
|
|
||||||
|
/* low-level interface to diffcore_order */
|
||||||
|
struct obj_order {
|
||||||
|
void *obj; /* setup by caller */
|
||||||
|
|
||||||
|
/* setup/used by order_objects() */
|
||||||
|
int orig_order;
|
||||||
|
int order;
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef const char *(*obj_path_fn_t)(void *obj);
|
||||||
|
|
||||||
|
void order_objects(const char *orderfile, obj_path_fn_t obj_path,
|
||||||
|
struct obj_order *objs, int nr);
|
||||||
|
|
||||||
#define DIFF_DEBUG 0
|
#define DIFF_DEBUG 0
|
||||||
#if DIFF_DEBUG
|
#if DIFF_DEBUG
|
||||||
void diff_debug_filespec(struct diff_filespec *, int, const char *);
|
void diff_debug_filespec(struct diff_filespec *, int, const char *);
|
||||||
|
|
|
@ -97,4 +97,25 @@ do
|
||||||
'
|
'
|
||||||
done
|
done
|
||||||
|
|
||||||
|
test_expect_success 'setup for testing combine-diff order' '
|
||||||
|
git checkout -b tmp HEAD~ &&
|
||||||
|
create_files 3 &&
|
||||||
|
git checkout master &&
|
||||||
|
git merge --no-commit -s ours tmp &&
|
||||||
|
create_files 5
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success "combine-diff: no order (=tree object order)" '
|
||||||
|
git diff --name-only HEAD HEAD^ HEAD^2 >actual &&
|
||||||
|
test_cmp expect_none actual
|
||||||
|
'
|
||||||
|
|
||||||
|
for i in 1 2
|
||||||
|
do
|
||||||
|
test_expect_success "combine-diff: orderfile using option ($i)" '
|
||||||
|
git diff -Oorder_file_$i --name-only HEAD HEAD^ HEAD^2 >actual &&
|
||||||
|
test_cmp expect_$i actual
|
||||||
|
'
|
||||||
|
done
|
||||||
|
|
||||||
test_done
|
test_done
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
test_description='combined diff show only paths that are different to all parents'
|
||||||
|
|
||||||
|
. ./test-lib.sh
|
||||||
|
|
||||||
|
# verify that diffc.expect matches output of
|
||||||
|
# `git diff -c --name-only HEAD HEAD^ HEAD^2`
|
||||||
|
diffc_verify () {
|
||||||
|
git diff -c --name-only HEAD HEAD^ HEAD^2 >diffc.actual &&
|
||||||
|
test_cmp diffc.expect diffc.actual
|
||||||
|
}
|
||||||
|
|
||||||
|
test_expect_success 'trivial merge - combine-diff empty' '
|
||||||
|
for i in $(test_seq 1 9)
|
||||||
|
do
|
||||||
|
echo $i >$i.txt &&
|
||||||
|
git add $i.txt
|
||||||
|
done &&
|
||||||
|
git commit -m "init" &&
|
||||||
|
git checkout -b side &&
|
||||||
|
for i in $(test_seq 2 9)
|
||||||
|
do
|
||||||
|
echo $i/2 >>$i.txt
|
||||||
|
done &&
|
||||||
|
git commit -a -m "side 2-9" &&
|
||||||
|
git checkout master &&
|
||||||
|
echo 1/2 >1.txt &&
|
||||||
|
git commit -a -m "master 1" &&
|
||||||
|
git merge side &&
|
||||||
|
>diffc.expect &&
|
||||||
|
diffc_verify
|
||||||
|
'
|
||||||
|
|
||||||
|
|
||||||
|
test_expect_success 'only one trully conflicting path' '
|
||||||
|
git checkout side &&
|
||||||
|
for i in $(test_seq 2 9)
|
||||||
|
do
|
||||||
|
echo $i/3 >>$i.txt
|
||||||
|
done &&
|
||||||
|
echo "4side" >>4.txt &&
|
||||||
|
git commit -a -m "side 2-9 +4" &&
|
||||||
|
git checkout master &&
|
||||||
|
for i in $(test_seq 1 9)
|
||||||
|
do
|
||||||
|
echo $i/3 >>$i.txt
|
||||||
|
done &&
|
||||||
|
echo "4master" >>4.txt &&
|
||||||
|
git commit -a -m "master 1-9 +4" &&
|
||||||
|
test_must_fail git merge side &&
|
||||||
|
cat <<-\EOF >4.txt &&
|
||||||
|
4
|
||||||
|
4/2
|
||||||
|
4/3
|
||||||
|
4master
|
||||||
|
4side
|
||||||
|
EOF
|
||||||
|
git add 4.txt &&
|
||||||
|
git commit -m "merge side (2)" &&
|
||||||
|
echo 4.txt >diffc.expect &&
|
||||||
|
diffc_verify
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'merge introduces new file' '
|
||||||
|
git checkout side &&
|
||||||
|
for i in $(test_seq 5 9)
|
||||||
|
do
|
||||||
|
echo $i/4 >>$i.txt
|
||||||
|
done &&
|
||||||
|
git commit -a -m "side 5-9" &&
|
||||||
|
git checkout master &&
|
||||||
|
for i in $(test_seq 1 3)
|
||||||
|
do
|
||||||
|
echo $i/4 >>$i.txt
|
||||||
|
done &&
|
||||||
|
git commit -a -m "master 1-3 +4hello" &&
|
||||||
|
git merge side &&
|
||||||
|
echo "Hello World" >4hello.txt &&
|
||||||
|
git add 4hello.txt &&
|
||||||
|
git commit --amend &&
|
||||||
|
echo 4hello.txt >diffc.expect &&
|
||||||
|
diffc_verify
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'merge removed a file' '
|
||||||
|
git checkout side &&
|
||||||
|
for i in $(test_seq 5 9)
|
||||||
|
do
|
||||||
|
echo $i/5 >>$i.txt
|
||||||
|
done &&
|
||||||
|
git commit -a -m "side 5-9" &&
|
||||||
|
git checkout master &&
|
||||||
|
for i in $(test_seq 1 3)
|
||||||
|
do
|
||||||
|
echo $i/4 >>$i.txt
|
||||||
|
done &&
|
||||||
|
git commit -a -m "master 1-3" &&
|
||||||
|
git merge side &&
|
||||||
|
git rm 4.txt &&
|
||||||
|
git commit --amend &&
|
||||||
|
echo 4.txt >diffc.expect &&
|
||||||
|
diffc_verify
|
||||||
|
'
|
||||||
|
|
||||||
|
test_done
|
Загрузка…
Ссылка в новой задаче